Squashed 'graphics-components-src/' content from commit 247be9ce

git-subtree-dir: graphics-components-src
git-subtree-split: 247be9ce40bd338d3934534fb6392504a0cdc81f
This commit is contained in:
Ben Aultowski 2026-02-25 21:21:50 -05:00
commit d04c218f36
593 changed files with 54517 additions and 0 deletions

8
.changeset/README.md Normal file
View file

@ -0,0 +1,8 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)

11
.changeset/config.json Normal file
View file

@ -0,0 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.0.2/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": true,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}

1
.github/CODEOWNERS vendored Normal file
View file

@ -0,0 +1 @@
* @hobbes7878

9
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,9 @@
### What's in this pull request
Tell us what this PR does or link to any related issues that describe the goal here.
### Before submitting, please check that you've ...
- [x] Read our [contributing guide](https://github.com/reuters-graphics/graphics-components/blob/master/CONTRIBUTING.md)
- [ ] Documented any new components or features
- [ ] Tagged an editor to review this PR

29
.github/workflows/check.yaml vendored Normal file
View file

@ -0,0 +1,29 @@
name: Check
on:
pull_request:
branches:
- main
jobs:
svelte-check:
name: Run svelte-check
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Install dependencies
run: |
pnpm i
pnpm svelte-kit sync
- name: Run svelte-check
run: pnpm svelte-check --output machine

31
.github/workflows/chromatic.yaml vendored Normal file
View file

@ -0,0 +1,31 @@
name: 'Chromatic'
on:
pull_request:
branches:
- main
jobs:
chromatic:
name: Run Chromatic
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: 22.12.0
cache: pnpm
- name: Install dependencies
run: pnpm i
- name: Run Chromatic
uses: chromaui/action@latest
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
buildScriptName: 'build:docs'
# exitZeroOnChanges: false

11
.github/workflows/docs.yaml vendored Normal file
View file

@ -0,0 +1,11 @@
name: Manually publish docs
on:
workflow_dispatch:
jobs:
docs:
uses: reuters-graphics/action-workflows/.github/workflows/docs.yaml@main
secrets: inherit
with:
node_version: '20'

11
.github/workflows/lint.yaml vendored Normal file
View file

@ -0,0 +1,11 @@
name: Lint
on:
pull_request:
branches:
- main
jobs:
lint:
uses: reuters-graphics/action-workflows/.github/workflows/lint.yaml@main
secrets: inherit

32
.github/workflows/pkg.pr.new.yaml vendored Normal file
View file

@ -0,0 +1,32 @@
name: Publish preview
on:
push:
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
- name: Publish to pkg.pr.new
run: pnpm dlx pkg-pr-new publish --packageManager=pnpm

29
.github/workflows/release.yaml vendored Normal file
View file

@ -0,0 +1,29 @@
name: Release
on:
push:
branches:
- main
jobs:
release:
uses: reuters-graphics/action-workflows/.github/workflows/changesets-release.yaml@main
secrets: inherit
with:
node_version: '20'
publish_docs: true
notify-downstream:
needs: release
if: needs.release.outputs.published == 'true'
runs-on: ubuntu-latest
steps:
- name: Wait for npm propagation
run: sleep 30
- name: Dispatch to bluprint_graphics-kit
uses: peter-evans/repository-dispatch@v4
with:
token: ${{ secrets.REPO_PAT_TOKEN }}
repository: reuters-graphics/bluprint_graphics-kit
event-type: dependency-updated

194
.gitignore vendored Normal file
View file

@ -0,0 +1,194 @@
# Created by https://www.toptal.com/developers/gitignore/api/node,macos,linux
# Edit at https://www.toptal.com/developers/gitignore?templates=node,macos,linux
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Node Patch ###
# Serverless Webpack directories
.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
.svelte-kit
# End of https://www.toptal.com/developers/gitignore/api/node,macos,linux
*storybook.log

1
.npmrc Normal file
View file

@ -0,0 +1 @@
engine-strict=true

5
.prettierignore Normal file
View file

@ -0,0 +1,5 @@
docs
dist
node_modules
!src/docs/
pnpm-lock.yaml

10
.prettierrc.js Normal file
View file

@ -0,0 +1,10 @@
import { svelte as svelteConfig } from '@reuters-graphics/yaks-prettier';
/**
* @type {import("prettier").Config}
*/
const config = {
...svelteConfig,
};
export default config;

9
.storybook/Theme.ts Normal file
View file

@ -0,0 +1,9 @@
import { create } from '@storybook/theming';
export default create({
base: 'light',
brandTitle: 'Reuters Graphics components',
brandUrl: 'https://reuters-graphics.github.io/graphics-components/',
brandImage: './logo.svg',
brandTarget: '_self',
});

12
.storybook/Wrapper.svelte Normal file
View file

@ -0,0 +1,12 @@
<script>
import Article from '../src/components/Article/Article.svelte';
import Theme from '../src/components/Theme/Theme.svelte';
import 'prism-themes/themes/prism-nord.css';
</script>
<Theme>
<Article>
<slot />
</Article>
</Theme>

18
.storybook/main.ts Normal file
View file

@ -0,0 +1,18 @@
import type { StorybookConfig } from '@storybook/sveltekit';
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|ts|svelte)'],
addons: [
'@storybook/addon-svelte-csf',
'@storybook/addon-essentials',
'@chromatic-com/storybook',
'@storybook/addon-interactions',
'@storybook/addon-a11y',
'storybook-addon-rtl',
],
framework: {
name: '@storybook/sveltekit',
options: {},
},
};
export default config;

25
.storybook/manager.ts Normal file
View file

@ -0,0 +1,25 @@
import { addons } from '@storybook/manager-api';
import theme from './Theme';
addons.setConfig({
isFullscreen: false,
showNav: true,
showPanel: true,
panelPosition: 'bottom',
enableShortcuts: true,
showToolbar: true,
selectedPanel: undefined,
initialActive: 'sidebar',
sidebar: {
showRoots: false,
collapsedRoots: ['other'],
},
toolbar: {
title: { hidden: false },
zoom: { hidden: false },
eject: { hidden: false },
copy: { hidden: false },
fullscreen: { hidden: false },
},
theme,
});

View file

@ -0,0 +1,13 @@
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0"
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;600&family=Inter+Tight:ital,wght@0,300;0,400;1,300;1,400&family=Inter:wght@300;400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<script>
window.global = window;
</script>

136
.storybook/preview.scss Normal file
View file

@ -0,0 +1,136 @@
@use './syntax.scss';
body {
font-family: 'Nunito Sans', Helvetica, Arial, sans-serif;
}
table.docblock-argstable {
p {
font-family: inherit;
font-size: inherit;
}
ul {
padding-inline-start: 20px;
}
}
div.sbdocs :where(p:not(.sb-anchor, .sb-unstyled, .sb-unstyled p)) {
font-size: 16px;
}
div.sbdocs-content {
h1:not(.sbdocs-preview *) {
font-family: 'Knowledge', sans-serif;
}
& > h2,
& > div > div > h2 {
margin-top: 4rem;
margin-bottom: 2rem;
&:first-of-type {
margin-top: 4rem;
margin-bottom: 2rem;
}
}
.docblock-source {
margin: 1rem 0 2.5rem;
}
& > div > :where(p, ul, ol),
.sb-anchor > div > :where(p, ul, ol) {
font-size: 18px;
line-height: 29px;
font-family: 'Knowledge', sans-serif;
.highlight {
background-color: rgb(254, 254, 160);
padding: 0 4px;
}
.bold {
font-weight: bold;
}
a {
font-family: 'Knowledge', sans-serif;
color: #0071a1;
text-decoration: none;
text-underline-offset: 2px;
&:hover {
text-decoration: underline;
}
}
}
}
.sbdocs-content {
blockquote:not(.sb-unstyled *) {
background-color: #ededed;
padding: 15px 20px;
border: 1px solid #ccc;
border-radius: 4px;
margin: 2rem auto;
p {
font-size: 16px;
line-height: 22px;
}
}
}
.sbdocs-content div.sbdocs:not(.sb-unstyled) {
font-family: 'Knowledge', sans-serif;
h1 {
font-family: 'Knowledge', sans-serif;
}
p,
ul,
li {
font-size: 18px;
line-height: 29px;
font-family: 'Knowledge', sans-serif;
.highlight {
background-color: rgb(254, 254, 160);
padding: 0 4px;
}
.bold {
font-weight: bold;
}
}
a {
font-family: 'Knowledge', sans-serif;
color: #0071a1;
text-decoration: none;
text-underline-offset: 2px;
&:hover {
text-decoration: underline;
}
}
&:not(.sbdocs-preview) {
code {
font-size: 90%;
margin-left: 2px;
margin-right: 2px;
background-color: #efefef;
padding: 2px 4px;
}
img {
display: block;
margin-top: 1rem;
margin-bottom: 2rem;
}
blockquote {
code {
background-color: #d9d9d9;
}
}
}
}
div.reset-article {
width: calc(100% + 30px);
margin-left: -15px;
}

68
.storybook/preview.ts Normal file
View file

@ -0,0 +1,68 @@
import '../src/scss/main.scss';
import './preview.scss';
import { SyntaxHighlighter } from '@storybook/components';
import Wrapper from './Wrapper.svelte';
import markdown from 'react-syntax-highlighter/dist/esm/languages/prism/markdown';
import scss from 'react-syntax-highlighter/dist/esm/languages/prism/scss';
import svelte from './svelte-highlighting.js';
import type { Preview } from '@storybook/svelte';
SyntaxHighlighter.registerLanguage('scss', scss);
SyntaxHighlighter.registerLanguage('svelte', svelte);
SyntaxHighlighter.registerLanguage('markdown', markdown);
const preview: Preview = {
// @ts-ignore Is OK
decorators: [() => Wrapper],
tags: ['autodocs', 'autodocs', 'autodocs', 'autodocs'],
parameters: {
viewMode: 'docs',
previewTabs: { 'storybook/docs/panel': { index: -1 } },
controls: {
expanded: true,
sort: 'requiredFirst',
matchers: {
color: /(background|colour|Colour)$/i,
date: /Date$/,
},
},
layout: 'fullscreen',
options: {
// https://storybook.js.org/docs/writing-stories/naming-components-and-hierarchy#sorting-stories
storySort: {
method: 'alphabetical-by-kind',
includeNames: false,
order: [
'Intro',
'Guides',
[
'Using these docs',
'Using with the graphics kit',
'Using with ArchieML docs',
'Customising components with SCSS',
'*',
'Getting help',
],
'Components',
['*', ['Intro', '*']],
'Styles',
[
'Intro',
'Colours',
['Intro', 'Primary', 'Thematic', '*'],
'Tokens',
['Intro', 'Typography', '*'],
],
'Actions',
['Intro', '*'],
'Contributing',
['Quickstart', 'Component Basics', 'Writing Stories', '*'],
],
},
},
},
};
export default preview;

7
.storybook/svelte-highlighting.d.ts vendored Normal file
View file

@ -0,0 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
declare function svelte(Prism: any): void;
declare namespace svelte {
let displayName: string;
let aliases: any[];
}
export default svelte;

View file

@ -0,0 +1,149 @@
svelte.displayName = 'svelte';
svelte.aliases = [];
export default function svelte(Prism) {
const blocks = '(if|else if|await|then|catch|each|html|debug)';
Prism.languages.svelte = Prism.languages.extend('markup', {
each: {
pattern: new RegExp(
'{[#/]each' +
'(?:(?:\\{(?:(?:\\{(?:[^{}])*\\})|(?:[^{}]))*\\})|(?:[^{}]))*}'
),
inside: {
'language-javascript': [
{
pattern: /(as[\s\S]*)\([\s\S]*\)(?=\s*\})/,
lookbehind: true,
inside: Prism.languages['javascript'],
},
{
pattern: /(as[\s]*)[\s\S]*(?=\s*)/,
lookbehind: true,
inside: Prism.languages['javascript'],
},
{
pattern: /(#each[\s]*)[\s\S]*(?=as)/,
lookbehind: true,
inside: Prism.languages['javascript'],
},
],
keyword: /[#/]each|as/,
punctuation: /{|}/,
},
},
block: {
pattern: new RegExp(
'{[#:/@]/s' +
blocks +
'(?:(?:\\{(?:(?:\\{(?:[^{}])*\\})|(?:[^{}]))*\\})|(?:[^{}]))*}'
),
inside: {
punctuation: /^{|}$/,
keyword: [new RegExp('[#:/@]' + blocks + '( )*'), /as/, /then/],
'language-javascript': {
pattern: /[\s\S]*/,
inside: Prism.languages['javascript'],
},
},
},
tag: {
pattern:
/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?:"[^"]*"|'[^']*'|{[\s\S]+?}(?=[\s/>])))|(?=[\s/>])))+)?\s*\/?>/i,
greedy: true,
inside: {
tag: {
pattern: /^<\/?[^\s>\/]+/i,
inside: {
punctuation: /^<\/?/,
namespace: /^[^\s>\/:]+:/,
},
},
'language-javascript': {
pattern:
/\{(?:(?:\{(?:(?:\{(?:[^{}])*\})|(?:[^{}]))*\})|(?:[^{}]))*\}/,
inside: Prism.languages['javascript'],
},
'attr-value': {
pattern: /=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/i,
inside: {
punctuation: [
/^=/,
{
pattern: /^(\s*)["']|["']$/,
lookbehind: true,
},
],
'language-javascript': {
pattern: /{[\s\S]+}/,
inside: Prism.languages['javascript'],
},
},
},
punctuation: /\/?>/,
'attr-name': {
pattern: /[^\s>\/]+/,
inside: {
namespace: /^[^\s>\/:]+:/,
},
},
},
},
'language-javascript': {
pattern: /\{(?:(?:\{(?:(?:\{(?:[^{}])*\})|(?:[^{}]))*\})|(?:[^{}]))*\}/,
lookbehind: true,
inside: Prism.languages['javascript'],
},
});
Prism.languages.svelte['tag'].inside['attr-value'].inside['entity'] =
Prism.languages.svelte['entity'];
Prism.hooks.add('wrap', (env) => {
if (env.type === 'entity') {
env.attributes['title'] = env.content.replace(/&amp;/, '&');
}
});
Object.defineProperty(Prism.languages.svelte.tag, 'addInlined', {
value: function addInlined(tagName, lang) {
const includedCdataInside = {};
includedCdataInside['language-' + lang] = {
pattern: /(^<!\[CDATA\[)[\s\S]+?(?=\]\]>$)/i,
lookbehind: true,
inside: Prism.languages[lang],
};
includedCdataInside['cdata'] = /^<!\[CDATA\[|\]\]>$/i;
const inside = {
'included-cdata': {
pattern: /<!\[CDATA\[[\s\S]*?\]\]>/i,
inside: includedCdataInside,
},
};
inside['language-' + lang] = {
pattern: /[\s\S]+/,
inside: Prism.languages[lang],
};
const def = {};
def[tagName] = {
pattern: RegExp(
/(<__[\s\S]*?>)(?:<!\[CDATA\[[\s\S]*?\]\]>\s*|[\s\S])*?(?=<\/__>)/.source.replace(
/__/g,
tagName
),
'i'
),
lookbehind: true,
greedy: true,
inside,
};
Prism.languages.insertBefore('svelte', 'cdata', def);
},
});
Prism.languages.svelte.tag.addInlined('style', 'css');
Prism.languages.svelte.tag.addInlined('script', 'javascript');
}

145
.storybook/syntax.scss Normal file
View file

@ -0,0 +1,145 @@
/**
* Nord theme styling for source code in docs
*/
.docblock-source {
border: 6px solid #333 !important;
overflow: hidden;
border-radius: 6px !important;
box-shadow:
0 10px 20px rgba(0, 0, 0, 0.19),
0 6px 6px rgba(0, 0, 0, 0.23) !important;
padding: 0 !important;
button {
background-color: #0071a1;
color: white;
border-top-left-radius: 0;
&:focus {
box-shadow: #4ee8c4 0 -3px 0 0 inset;
}
}
}
div pre.prismjs {
background-color: #2e3440 !important;
color: #f8f8f2;
code[class*='language-'],
pre[class*='language-'] {
color: #f8f8f2;
background: none;
font-family:
'Fira Code', Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
/* Code blocks */
pre[class*='language-'] {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
border-radius: 0.3em;
}
:not(pre) > code[class*='language-'],
pre[class*='language-'] {
background: #2e3440;
}
/* Inline code */
:not(pre) > code[class*='language-'] {
padding: 0.1em;
border-radius: 0.3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #9199aa;
}
.token.punctuation {
color: #81a1c1;
}
.namespace {
opacity: 0.7;
}
.token.property,
.token.tag,
.token.constant,
.token.symbol,
.token.deleted {
color: #81a1c1;
}
.token.number {
color: #b48ead;
}
.token.boolean {
color: #81a1c1;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #a3be8c;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string,
.token.variable {
color: #81a1c1;
}
.token.atrule,
.token.attr-value,
.token.function,
.token.class-name {
color: #88c0d0;
}
.token.keyword {
color: #81a1c1;
}
.token.regex,
.token.important {
color: #ebcb8b;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
}

7
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,7 @@
{
"recommendations": [
"unifiedjs.vscode-mdx",
"somewhatstationery.some-sass",
"svelte.svelte-vscode"
]
}

18
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,18 @@
{
"eslint.validate": ["javascript", "javascriptreact", "svelte", "jsx"],
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"emmet.excludeLanguages": ["markdown", "scss"],
"files.associations": {
"*.svx": "mdx"
},
"[mdx]": {
"editor.wordWrap": "on"
},
"[svelte]": {
"editor.defaultFormatter": "svelte.svelte-vscode"
},
"typescript.tsdk": "node_modules/typescript/lib"
}

8
.vscode/svelte.styles.code-snippets vendored Normal file
View file

@ -0,0 +1,8 @@
{
"Svelte SCSS style": {
"scope": "svelte",
"prefix": "scss",
"body": ["<style lang=\"scss\">", "$1", "</style>"],
"description": "Add a Svelte SCSS style tag",
},
}

278
CHANGELOG.md Normal file
View file

@ -0,0 +1,278 @@
# @reuters-graphics/graphics-components
## 3.2.1
### Patch Changes
- 94c2346: Fixes fontface definitions for Newsreader typeface to fix issue with bold and bolder weights.
## 3.2.0
### Minor Changes
- df7c622: Adding a basic pmtiles map component
## 3.1.0
### Minor Changes
- 0f9248c: Replaces FreightText with Newsreader Text font
## 3.0.27
### Patch Changes
- 6dc6e54: Removes demo files, docs and stories from the published package.
## 3.0.26
### Patch Changes
- 27d07e4: Adds Lottie component
- f9aec45: Adds HorizontalScroller component
## 3.0.25
### Patch Changes
- 257f967: Updates svelte-fa version
## 3.0.24
### Patch Changes
- 0fce4cd: Removes dev from $app/environment
## 3.0.23
### Patch Changes
- 022d0dc: Test downstream notification workflow with updated reusable workflow
## 3.0.22
### Patch Changes
- bf550d5: Test downstream workflow notification system
## 3.0.21
### Patch Changes
- a2e6e8d: Fixes a bug in PhotoPack that on earlier iPhones would break. Also adds smarter default layouts based on the number of images in the pack and the max width of the PhotoPack.
## 3.0.20
### Patch Changes
- 6d5c152: Removes stray Google Analytics loading call so GA is only loaded via Google Tag Manager.
## 3.0.19
### Patch Changes
- 627f267: Enhances progress reactivity for ScrollerVideo
## 3.0.18
### Patch Changes
- 1402aac: Fix for GTM tags
## 3.0.17
### Patch Changes
- cd0dc83: Updates analytics scripts to work with GDPR-compliant GTM container
## 3.0.16
### Patch Changes
- 2b6b4f4: Fixes prettifyDate to format Aug, Oct-Dec
## 3.0.15
### Patch Changes
- 68b51a1: Adds util function prettifyDate
## 3.0.14
### Patch Changes
- fdc3c6b: Moves dependencies to dependencies from devDependencies
## 3.0.13
### Patch Changes
- 36d5896: Adds ScrollerVideo
## 3.0.12
### Patch Changes
- 771ccb4: Fixes sizing issue for Framer
## 3.0.11
### Patch Changes
- 31caab2: Fix typos in Scroller
## 3.0.10
### Patch Changes
- cf7e513: Updated oneTrustId to production ID
## 3.0.9
### Patch Changes
- 18e49eb: renames Theme and CustomTheme types
## 3.0.8
### Patch Changes
- bdf3918: Updates Visible to allow unit specification for top, bototm, right, left and adds a demo
## 3.0.7
### Patch Changes
- f80e450: Removes Parsely page analytics
## 3.0.6
### Patch Changes
- a205a35: Adds new Headpile component.
## 3.0.5
### Patch Changes
- f41f79b: Creates ScrollerBase component, which is used in Scroller and can be used to make custom scrollytelling components.
## 3.0.4
### Patch Changes
- 06beea8: Update tokens and component layouts to accomodate margins and paddings for RTL webpages
## 3.0.3
### Patch Changes
- c074a18: Allows step.background to be undefined
## 3.0.2
### Patch Changes
- 737f2e1: Adds a simple dropdown option to search embeds for Framer
- a032218: Don't fetch referrals on non-dotcom domains
- 2d4a641: Cleans up a spare console log left in
- c91807e: a11y fixes for SiteHeader and SiteFooter
- b13463f: fixes for Ad types that were colliding with their component names and a bug in the Framer Resizer
- a48d333: Ignore hydration mismatch in SEO component ld+json
## 3.0.1
### Patch Changes
- 93a41f3: Exposes bindable props for the Scroller component
## 3.0.0
### Major Changes
- 7432131: Svelte 5.0+ components.
#### What's in it?
3.0 updates all graphics components to [Svelte 5 syntax](https://svelte.dev/docs/svelte/v5-migration-guide).
Components are now only compatible with Svelte 5-based versions of the graphics kit, starting with [1.1.0](https://github.com/reuters-graphics/bluprint_graphics-kit/blob/main/CHANGELOG.md#110).
## 2.0.3
### Patch Changes
- 50f4320: Updates favicons for new kinesis
## 2.0.2
### Patch Changes
- 64b6d88: Fix for changed thumbnail API schema
## 2.0.1
### Patch Changes
- ca278c4: ReferralBlock checks if a referral is for the current page and doesn't include it if so.
## 2.0.0
### Major Changes
- 3e20529: Removes Google Docs-based utils in favour of ArchieML/RNGS.io examples.
### Patch Changes
- a5ad543: Docs syntax highlighting and organisation
- 06b4d48: Updates ReferralBlock and SiteFooter
- b44ed64: Reuters Graphics logo refresh
- 05b80fd: Patches up component docs links
## 1.1.7
### Patch Changes
- 9dec472: Background change to action workflows...
## 1.1.6
### Patch Changes
- d7d88e1: Adds parsely analytics
## 1.1.5
### Patch Changes
- 2217848: Adds a skip link to the header component
## 1.1.4
### Patch Changes
- 4788ee2: Adds Dotcom Knowledge font aliases needed for ads
## 1.1.3
### Patch Changes
- 146b970: Fixes "more" menu options
## 1.1.2
### Patch Changes
- d459852: Ads will collapse if unfulfilled
## 1.1.1
### Patch Changes
- 23b001b: Excludes Google ads from iframe reset
## 1.1.0
### Minor Changes
- 6e2b8a7: Updates Storybook and makes Svelte 4 minimum.

66
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,66 @@
# Contributing Guide
## Why this?
Most Reuters Graphics repos don't include (or need) contributing guidelines. The ones that do represent critical infrastructure. They also are the ones we most want wider contributions from the team to improve and maintain.
This doc provides for a few simple guidelines to make sure changes are well considered and represent the best ideas for how to move our tools forward while opening up the opportunity for others to ship their next great idea.
## Who can contribute?
Contributions are always welcome from members of the Reuters Graphics team.
Anyone outside our team using these components is welcome to submit PRs or issues, **BUT** if they are designed solely to benefit a use case that isn't ours, they likely won't be merged.
## How should Reuters Graphics staff contribute?
### 🏷️ Make an issue
We recommend your first step is to create an issue on this repo describing what is missing, broken or could be added or improved. (We'll close that issue when we merge your PR.)
- It's helpful if that issue describes what changes you propose to make at a high level so we can agree on a general direction before you write code. That's especially true if code you're writing will change how others need to write theirs.
- If needed, provide any links to best practice guidelines that support the change you want to make, e.g., for making accessibility improvements to components.
- Tag others on the team who may have expertise or would contribute to any needed discussion.
- **Always tag an editor.**
### 🧹 Follow code standards
Once you're ready to submit code, be sure it's properly formatted _before_ you ask for a review and run our built-in code linters (eslint + prettier) over your changed files:
```console
pnpm lint && pnpm format
```
Any public methods or component props should be properly typed and documented with comments. (See existing components for examples.) For future developers, also add comments around any tricky bits of _internal_ logic you're adding.
### 📝 Write Storybook stories
All new components and component features should be accompanied by [Storybook stories](https://reuters-graphics.github.io/graphics-svelte-components/) included with your PR. See other components for examples of how to add them.
Be sure to target your Storybook docs for non-developers with real world examples of how to use components within the graphics kit.
### 🍺 Submit code with a changeset
All code contributions should be made through the normal [GitHub Flow](https://www.w3schools.com/git/git_github_flow.asp#:~:text=The%20GitHub%20flow%20is%20a,Make%20changes%20and%20add%20Commits). Basically, make a branch and submit a pull request.
(Generally, it's better to avoid bundling several new features or components in a single PR. Breaking them apart into smaller, individual contributions makes them easier to review and manage.)
Each PR should be accompanied by a [changeset](https://github.com/changesets/changesets). You can add one by running:
```console
pnpm changeset add
```
Once you've submitted your PR, tag an editor to review it.
An editor will approve your PR after addressing any issues they see. Once an editor approves and there are no conflicts or failing tests, you can merge your PR into master.
### Test in downstream projects
Once a PR is created, a testable version of the library is published via [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new). A comment will be added to your PR that documents how to install the library's test version. Use it to test any new components or features in the graphics kit.
### ✉️ Publishing to the team
Publishing is handled via [changesets](https://github.com/changesets/changesets) and should follow [semantic versioning](https://semver.org/) conventions. Most MINOR and all MAJOR version changes should be identified ahead of time during PR review.
Once a new version of the library is published, a [PR will be created in the graphics kit](https://github.com/reuters-graphics/bluprint_graphics-kit/pulls) to update this dependency. Merge that.

9
README.md Normal file
View file

@ -0,0 +1,9 @@
![](https://graphics.thomsonreuters.com/style-assets/images/logos/reuters-graphics-logo/svg/graphics-logo-color-dark.svg)
# ⚙️ graphics-components
[![npm version](https://badge.fury.io/js/@reuters-graphics%2Fgraphics-components.svg)](https://badge.fury.io/js/@reuters-graphics%2Fgraphics-components)
Svelte components, SCSS and more for Reuters Graphics pages.
[Read the docs.](https://reuters-graphics.github.io/graphics-components/)

6
chromatic.config.json Normal file
View file

@ -0,0 +1,6 @@
{
"onlyChanged": true,
"projectId": "Project:64a5c42823795823edcb60f4",
"zip": true,
"buildScriptName": "build:docs"
}

50
eslint.config.js Normal file
View file

@ -0,0 +1,50 @@
import { svelte } from '@reuters-graphics/yaks-eslint';
import reactPlugin from 'eslint-plugin-react';
import * as mdx from 'eslint-plugin-mdx';
import storybook from 'eslint-plugin-storybook';
export default [
{
files: ['src/**/*.{js,ts,svelte,jsx,tsx,mdx}', '.storybook/**/*'],
},
{
ignores: [
'node_modules/',
'docs/',
'dist/',
'.storybook/svelte-highlighting.js',
'bin/css-to-js/',
'bin/newComponent/',
'.svelte-kit/',
'src/docs/guides/archieml.mdx',
],
},
...svelte,
...storybook.configs['flat/recommended'],
reactPlugin.configs.flat.recommended,
{
settings: { react: { version: '18.2' } },
rules: {
'react/prop-types': [
'error',
{
skipUndeclared: true,
},
],
},
},
{
...mdx.flat,
processor: mdx.createRemarkProcessor({
lintCodeBlocks: true,
}),
},
{
...mdx.flatCodeBlocks,
rules: {
...mdx.flatCodeBlocks.rules,
'no-undef': 'off',
'@typescript-eslint/no-unused-vars': 'off',
},
},
];

14
knip.config.ts Normal file
View file

@ -0,0 +1,14 @@
import type { KnipConfig } from 'knip';
const config: KnipConfig = {
entry: ['src/index.js', 'src/**/*.stories.{svelte,mdx}', 'src/docs/**'],
project: [
'src/**/*.{mdx,js,jsx,ts,svelte}',
'bin/**/*.{js,cjs}',
'.storybook/**/*.{js,ts,svelte}',
],
ignore: ['**/*.d.ts'],
ignoreDependencies: [/@types\/.*/, 'chromatic', 'prop-types', 'postcss'],
};
export default config;

143
package.json Normal file
View file

@ -0,0 +1,143 @@
{
"name": "@reuters-graphics/graphics-components",
"version": "3.2.1",
"type": "module",
"private": false,
"homepage": "https://reuters-graphics.github.io/graphics-components",
"repository": {
"type": "git",
"url": "git+https://github.com/reuters-graphics/graphics-components.git"
},
"packageManager": "pnpm@9.13.2",
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "public"
},
"scripts": {
"start": "storybook dev -p 3000",
"lint": "eslint --fix",
"format": "prettier . --write",
"build": "rimraf ./dist && svelte-package -i ./src && publint",
"prepare": "svelte-package -i ./src",
"build:docs": "storybook build -o docs",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"changeset:version": "changeset version",
"changeset:publish": "git add --all && changeset publish",
"knip": "knip",
"test": "vitest"
},
"license": "MIT",
"files": [
"dist",
"!dist/**/*.stories.*",
"!dist/**/*.mdx",
"!dist/**/demo",
"!dist/docs",
"!dist/**/*.test.*",
"!dist/**/*.spec.*",
"!dist/**/*.mp4",
"!dist/**/*.mov",
"!dist/**/images"
],
"engines": {
"node": ">=20.18"
},
"peerDependencies": {
"svelte": "^5.0.0"
},
"devDependencies": {
"@changesets/cli": "^2.29.2",
"@chromatic-com/storybook": "^3.2.6",
"@reuters-graphics/yaks-eslint": "^0.1.1",
"@reuters-graphics/yaks-prettier": "^0.1.1",
"@storybook/addon-a11y": "^8.6.12",
"@storybook/addon-essentials": "^8.6.12",
"@storybook/addon-interactions": "^8.6.12",
"@storybook/addon-svelte-csf": "5.0.0-next.28",
"@storybook/blocks": "^8.6.12",
"@storybook/components": "^8.6.12",
"@storybook/manager-api": "^8.6.12",
"@storybook/svelte": "^8.6.12",
"@storybook/sveltekit": "^8.6.12",
"@storybook/test": "^8.6.12",
"@storybook/theming": "^8.6.12",
"@sveltejs/package": "^2.3.11",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@types/css": "^0.0.37",
"@types/eslint": "^9.6.1",
"@types/fs-extra": "^11.0.4",
"@types/google-publisher-tag": "^1.20250210.0",
"@types/gtag.js": "^0.0.12",
"@types/lodash-es": "^4.17.12",
"@types/mdx": "^2.0.13",
"@types/node": "^22.14.1",
"@types/prompts": "^2.4.9",
"@types/proper-url-join": "^2.1.5",
"@types/pym.js": "^1.3.2",
"@types/react": "^18.3.20",
"@types/react-syntax-highlighter": "^15.5.13",
"chromatic": "^11.28.2",
"css": "^3.0.0",
"css-color-converter": "^2.0.0",
"deep-object-diff": "^1.1.9",
"eslint": "^9.25.0",
"eslint-plugin-mdx": "^3.4.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-storybook": "^0.12.0",
"knip": "^5.50.5",
"mermaid": "^10.9.3",
"postcss": "^8.5.3",
"prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.3.3",
"prism-themes": "^1.9.0",
"prop-types": "^15.8.1",
"publint": "^0.3.12",
"react": "^18.3.1",
"react-colorful": "^5.6.1",
"react-dom": "^18.3.1",
"react-syntax-highlighter": "^15.6.1",
"rimraf": "^6.0.1",
"sass": "^1.86.3",
"storybook": "^8.6.12",
"svelte": "^5.28.1",
"svelte-check": "^4.1.6",
"typescript": "^5.8.3",
"vite": "^6.3.2"
},
"dependencies": {
"@fortawesome/free-regular-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@lottiefiles/dotlottie-web": "^0.52.2",
"@reuters-graphics/svelte-markdown": "^0.0.3",
"@sveltejs/kit": "^2.0.0",
"@types/geojson": "^7946.0.16",
"dayjs": "^1.11.13",
"es-toolkit": "^1.35.0",
"journalize": "^2.6.0",
"maplibre-gl": "^5.15.0",
"mp4box": "^0.5.4",
"pmtiles": "^4.3.2",
"proper-url-join": "^2.1.2",
"pym.js": "^1.3.2",
"slugify": "^1.6.6",
"storybook-addon-rtl": "^1.1.0",
"svelte-fa": "^4.0.4",
"svelte-intersection-observer": "^1.0.0",
"ua-parser-js": "^2.0.3",
"vitest": "^3.2.4"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"svelte": "./dist/index.js",
"default": "./dist/index.js"
},
"./scss/*": "./dist/scss/*"
},
"svelte": "./dist/index.js",
"types": "./dist/index.d.ts",
"bugs": {
"url": "https://github.com/reuters-graphics/graphics-components/issues"
}
}

9313
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

67
public/logo.svg Normal file
View file

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1608.56 474.84">
<defs>
<style>
.cls-2 {
fill: #D64000;
}
.cls-3 {
fill: #212223;
}
</style>
</defs>
<g id="Layer_1-2" data-name="Layer 1">
<g>
<path class="cls-2" d="M410.49,63.73c6.04,6.05,9.39,14.13,9.39,22.68s-3.32,16.64-9.39,22.68c-12.49,12.5-32.83,12.5-45.32,0-6.04-6.05-9.39-14.1-9.39-22.68s3.32-16.64,9.39-22.68c6.26-6.27,14.46-9.4,22.66-9.4s16.43,3.13,22.66,9.4Z"/>
<path class="cls-2" d="M63.85,411.02c-12.49-12.5-12.49-32.86,0-45.37,6.04-6.05,14.08-9.4,22.66-9.4s16.62,3.32,22.66,9.4c12.49,12.5,12.49,32.87,0,45.37-6.07,6.08-14.11,9.4-22.66,9.4s-16.62-3.32-22.66-9.4Z"/>
<path class="cls-2" d="M308.11,66.23l-.22-.09c-6.82-2.88-12.14-8.24-14.96-15.1-2.88-6.92-2.88-14.54,0-21.49,2.88-6.92,8.26-12.31,15.18-15.2,3.47-1.44,7.11-2.16,10.74-2.16s7.29.72,10.73,2.16c14.3,5.92,21.09,22.37,15.18,36.69-5.92,14.32-22.34,21.12-36.65,15.2Z"/>
<path class="cls-2" d="M155.61,406.45c3.63,0,7.26.72,10.7,2.13,14.3,5.92,21.09,22.37,15.18,36.69-2.88,6.93-8.26,12.34-15.18,15.2-6.92,2.88-14.52,2.88-21.47,0-14.3-5.92-21.09-22.37-15.18-36.69,2.88-6.92,8.26-12.34,15.18-15.2,3.47-1.44,7.1-2.16,10.73-2.16l.03.03Z"/>
<path class="cls-2" d="M261.2,24.06c0,12.91-10.2,23.47-22.94,24.06h-1.09c-13.27,0-24.04-10.81-24.04-24.06S223.93,0,237.17,0s24.04,10.84,24.04,24.09v-.03Z"/>
<path class="cls-2" d="M213.1,450.78c0-13.29,10.8-24.06,24.03-24.06s24.04,10.81,24.04,24.06-10.8,24.06-24.04,24.06-24.03-10.81-24.03-24.06Z"/>
<path class="cls-2" d="M163.34,58.84c-4.94,2.04-10.36,2.04-15.34,0-4.98-2.07-8.83-5.92-10.86-10.87-4.22-10.24.63-21.99,10.86-26.22,2.5-1.03,5.07-1.54,7.64-1.54,7.89,0,15.34,4.7,18.56,12.41,2.03,4.98,2.03,10.43,0,15.35-2.03,4.98-5.88,8.84-10.86,10.87Z"/>
<path class="cls-2" d="M310.97,416c2.5-1.04,5.1-1.57,7.7-1.57s5.2.53,7.67,1.53c4.98,2.07,8.83,5.92,10.86,10.87,2.04,4.98,2.04,10.43,0,15.35-2.03,4.98-5.88,8.83-10.86,10.87-4.97,2.04-10.39,2.04-15.36,0-4.98-2.04-8.83-5.89-10.86-10.87-2.03-4.98-2.03-10.4,0-15.35,2.04-4.95,5.89-8.8,10.86-10.84Z"/>
<path class="cls-2" d="M97.8,75.29c6.17,6.17,6.26,16.1.31,22.4l-.34.34c-6.26,6.27-16.46,6.27-22.72,0-6.26-6.27-6.26-16.48,0-22.75,3.13-3.13,7.23-4.7,11.36-4.7s8.23,1.57,11.36,4.7h.03Z"/>
<path class="cls-2" d="M376.34,376.9c3.1-3.13,7.23-4.7,11.33-4.7s8.23,1.57,11.36,4.7c3.04,3.01,4.69,7.05,4.69,11.34s-1.66,8.33-4.69,11.37c-6.26,6.3-16.43,6.27-22.72,0-3.04-3.04-4.69-7.11-4.69-11.37s1.66-8.3,4.69-11.34h.03Z"/>
<path class="cls-2" d="M32.61,174.29c-10.23-4.23-15.08-16.01-10.86-26.22,2.03-4.98,5.88-8.84,10.86-10.87,2.5-1,5.1-1.54,7.7-1.54s5.2.5,7.67,1.54c10.23,4.23,15.09,16.01,10.86,26.22-2.03,4.98-5.88,8.84-10.86,10.87-4.98,2.04-10.42,2.04-15.34,0h-.03Z"/>
<path class="cls-2" d="M441.79,300.61c4.97,2.04,8.82,5.89,10.86,10.87,2.03,4.98,2.03,10.4,0,15.35-4.23,10.21-15.96,15.1-26.2,10.84-4.98-2.04-8.82-5.89-10.86-10.87-2.03-4.98-2.03-10.4,0-15.35,3.19-7.71,10.67-12.38,18.56-12.38,2.54,0,5.13.5,7.64,1.54Z"/>
<path class="cls-2" d="M0,237.54c0-13.28,10.8-24.06,24.04-24.06s24.04,10.81,24.04,24.06-10.8,24.06-24.04,24.06S0,250.8,0,237.54Z"/>
<path class="cls-2" d="M426.26,237.54c0-13.28,10.8-24.06,24.04-24.06s24.04,10.81,24.04,24.06-10.8,24.06-24.04,24.06-24.04-10.81-24.04-24.06Z"/>
<path class="cls-2" d="M14.24,329.81c-5.92-14.32.88-30.73,15.18-36.69,6.92-2.88,14.52-2.88,21.47,0,6.92,2.88,12.3,8.27,15.18,15.2,2.88,6.93,2.88,14.54,0,21.49-2.88,6.92-8.26,12.31-15.18,15.2-6.92,2.88-14.52,2.88-21.47,0-6.92-2.88-12.3-8.27-15.18-15.2Z"/>
<path class="cls-2" d="M408.04,166.49c-2.85-6.92-2.85-14.54,0-21.46,2.88-6.92,8.26-12.31,15.18-15.2,3.47-1.47,7.1-2.16,10.67-2.16,11.02,0,21.47,6.52,25.95,17.33,5.92,14.32-.88,30.73-15.18,36.69-6.92,2.88-14.52,2.88-21.47,0-6.92-2.88-12.3-8.27-15.18-15.2h.03Z"/>
<path class="cls-2" d="M305.36,305.69c-8.79,8.77-8.79,23.09,0,31.86,8.79,8.8,23.07,8.8,31.86,0,4.26-4.26,6.6-9.9,6.6-15.92s-2.35-11.69-6.6-15.91c-4.38-4.39-10.14-6.58-15.9-6.58s-11.58,2.19-15.96,6.55h0Z"/>
<path class="cls-2" d="M117.55,223.79c-9.51-2.54-15.15-12.34-12.61-21.87,1.25-4.61,4.19-8.46,8.33-10.84,2.72-1.6,5.79-2.41,8.86-2.41,1.53,0,3.1.19,4.63.6,4.6,1.25,8.45,4.2,10.83,8.33,2.38,4.14,3,8.96,1.78,13.57-2.53,9.52-12.33,15.2-21.85,12.63h.03Z"/>
<path class="cls-2" d="M347.49,285.48c-4.6-1.25-8.45-4.2-10.83-8.33-2.38-4.14-3-8.96-1.78-13.57,1.25-4.61,4.19-8.46,8.32-10.84,2.75-1.6,5.82-2.41,8.89-2.41,1.28,0,2.56.13,3.82.41l.85.22c9.51,2.54,15.15,12.38,12.61,21.87-1.25,4.61-4.19,8.46-8.32,10.84-4.13,2.38-8.95,3.04-13.55,1.79v.03Z"/>
<path class="cls-2" d="M108.41,271.95c-1.97-7.36,2.16-14.91,9.3-17.3l.81-.22c7.61-2.07,15.46,2.51,17.49,10.12,1,3.7.5,7.55-1.44,10.87-1.91,3.29-5.01,5.67-8.67,6.67-3.69,1-7.51.5-10.86-1.44-3.29-1.91-5.66-5.01-6.67-8.68l.03-.03Z"/>
<path class="cls-2" d="M344.92,219.06l-.63-.38c-2.97-1.94-5.1-4.86-6.01-8.3-2.03-7.64,2.47-15.48,10.11-17.55,7.57-2.04,15.46,2.51,17.49,10.12,2.03,7.61-2.47,15.48-10.11,17.51-3.69,1-7.51.47-10.86-1.44v.03Z"/>
<path class="cls-2" d="M160.96,328.65l-.41.41c-4.19,4.2-11.02,4.2-15.21,0-4.19-4.2-4.19-10.99,0-15.2,4.19-4.2,11.02-4.2,15.21,0,2.03,2.04,3.16,4.73,3.16,7.61,0,2.69-.97,5.2-2.75,7.18Z"/>
<path class="cls-2" d="M328.84,145.53c2.03,2.04,3.16,4.73,3.16,7.61s-1.13,5.58-3.16,7.61c-4.19,4.2-11.02,4.2-15.21,0-2.03-2.07-3.16-4.73-3.16-7.61s1.13-5.58,3.16-7.61c2.07-2.04,4.73-3.16,7.61-3.16s5.57,1.1,7.61,3.13v.03Z"/>
<path class="cls-2" d="M218.7,345.32c1.91,3.29,2.41,7.18,1.44,10.87-1,3.7-3.35,6.77-6.67,8.68-3.32,1.91-7.17,2.41-10.86,1.44-7.61-2.04-12.14-9.9-10.11-17.51,1-3.7,3.35-6.77,6.67-8.68,3.29-1.91,7.14-2.41,10.86-1.44,3.69,1,6.76,3.35,8.67,6.67v-.03Z"/>
<path class="cls-2" d="M255.51,129.58c-1.91-3.29-2.41-7.17-1.44-10.87,1-3.7,3.35-6.77,6.67-8.68,2.19-1.25,4.63-1.91,7.1-1.91,1.25,0,2.47.16,3.72.47,3.69,1,6.76,3.35,8.67,6.67,1.91,3.29,2.41,7.17,1.44,10.87-2.03,7.61-9.89,12.16-17.49,10.12-3.69-1-6.76-3.35-8.67-6.67Z"/>
<path class="cls-2" d="M276.98,337.08l.75.44c3.73,2.41,6.38,6.08,7.54,10.37,2.54,9.52-3.1,19.33-12.61,21.87-4.63,1.26-9.42.6-13.55-1.78-4.13-2.38-7.07-6.24-8.32-10.84-1.25-4.61-.6-9.43,1.78-13.57,2.38-4.13,6.23-7.08,10.83-8.33,1.57-.41,3.13-.59,4.67-.59,3.1,0,6.13.81,8.89,2.41l.03.03Z"/>
<path class="cls-2" d="M210.97,139.64c-9.48,2.54-19.28-3.13-21.85-12.63-2.53-9.52,3.1-19.33,12.61-21.87,1.53-.41,3.07-.6,4.6-.6,7.89,0,15.12,5.26,17.24,13.22,2.53,9.52-3.1,19.33-12.61,21.87Z"/>
<path class="cls-2" d="M153.01,175.51c12.41,0,22.47-10.07,22.47-22.49s-10.06-22.49-22.47-22.49-22.47,10.07-22.47,22.49,10.06,22.49,22.47,22.49Z"/>
</g>
<g>
<g>
<path class="cls-3" d="M536.58,47.31h89.16c48.17,0,80.4,28.11,80.4,72.02,0,27.83-14.7,50.07-37.53,60.9l43.15,83.46h-51.91l-36.92-72.66h-39.42v72.66h-46.93V47.31ZM583.51,85.94v66.46h38.49c22.22,0,36.28-12.98,36.28-33.07s-14.06-33.39-36.28-33.39h-38.49Z"/>
<path class="cls-3" d="M718.44,184.54c0-49.47,32.83-81.59,82.57-81.59s80.72,29.66,81.33,76.35c0,4.65-.32,9.89-1.25,14.85h-116.68v2.15c.93,22.55,15.63,36.17,37.85,36.17,17.84,0,30.34-7.74,34.11-22.55h43.48c-5.02,30.93-32.83,55.63-75.7,55.63-53.48,0-85.71-31.84-85.71-80.99ZM837.93,165.68c-3.13-19.46-16.59-30.29-36.6-30.29s-33.47,11.44-35.97,30.29h72.57Z"/>
<path class="cls-3" d="M1009.81,104.84h44.12v158.89h-39.1l-4.06-19.17c-10.32,12.35-24.39,21.01-47.25,21.01-33.15,0-63.81-16.39-63.81-72.66v-88.1h44.12v82.23c0,28.43,9.4,42.36,31.3,42.36s34.72-15.76,34.72-45.14v-79.45l-.03.03Z"/>
<path class="cls-3" d="M1080.01,60.42h44.12v44.51h36.6v36.8h-36.6v69.56c0,11.12,4.38,15.77,15.63,15.77h22.83v36.77h-36.6c-30.66,0-45.97-15.13-45.97-45.42V60.42Z"/>
<path class="cls-3" d="M1169.86,184.54c0-49.47,32.83-81.59,82.57-81.59s80.72,29.66,81.33,76.35c0,4.65-.32,9.89-1.25,14.85h-116.68v2.15c.93,22.55,15.63,36.17,37.85,36.17,17.84,0,30.34-7.74,34.11-22.55h43.47c-5.02,30.93-32.83,55.63-75.7,55.63-53.48,0-85.7-31.84-85.7-80.99ZM1289.36,165.68c-3.13-19.46-16.59-30.29-36.6-30.29s-33.47,11.44-35.96,30.29h72.57Z"/>
<path class="cls-3" d="M1451.32,104.8v39.58h-17.52c-26.28,0-36.28,17.31-36.28,41.41v77.9h-44.12V104.8h40.06l4.06,23.79c8.76-14.21,21.26-23.79,46.29-23.79h7.51Z"/>
<path class="cls-3" d="M1500.28,210.87c1.57,13.9,13.74,23.5,35.35,23.5,18.45,0,29.73-5.88,29.73-16.99,0-12.98-10.94-13.9-36.92-17.31-37.53-4.33-66.62-12.67-66.62-45.42s27.84-52.25,69.11-51.93c43.16,0,72.89,18.86,75.09,51.3h-42.23c-1.25-12.67-13.46-20.41-31.29-20.41s-28.45,6.19-28.45,16.4c0,12.07,13.78,13.3,36.92,16.08,36.92,3.69,67.58,12.07,67.58,47.92,0,31.52-30.02,51.62-73.21,51.62s-75.06-20.72-76.94-54.71h41.91l-.03-.03Z"/>
</g>
<g>
<path class="cls-3" d="M534.98,369.02c0-8.94,1.29-17.05,3.87-24.34,2.58-7.28,6.39-13.56,11.44-18.83,5.04-5.27,11.27-9.34,18.66-12.21,7.4-2.86,15.91-4.3,25.54-4.3,13.76,0,25.57,2.06,35.43,6.19l-2.24,12.56c-4.82-1.6-9.83-2.95-15.05-4.04-5.22-1.09-10.87-1.63-16.94-1.63-14.68,0-25.94,3.96-33.8,11.87-7.86,7.91-11.78,19.55-11.78,34.92s3.84,26.09,11.52,34.23c7.68,8.14,18.63,12.21,32.85,12.21,3.55,0,7.14-.2,10.75-.6,3.61-.4,6.79-.89,9.55-1.46v-38.87h14.45v48.16c-2.06.69-4.42,1.35-7.05,1.98-2.64.63-5.48,1.17-8.51,1.63-3.04.46-6.17.83-9.37,1.12-3.21.29-6.36.43-9.46.43-9.86,0-18.55-1.49-26.06-4.47-7.51-2.98-13.76-7.11-18.75-12.38-4.99-5.27-8.75-11.49-11.27-18.66-2.52-7.16-3.78-14.99-3.78-23.48Z"/>
<path class="cls-3" d="M755.82,345.29c0,7.91-2.32,14.79-6.97,20.64-4.64,5.85-11.04,9.86-19.18,12.04l32.34,47.99h-18.23l-29.93-46.61h-28.04v46.61h-15.14v-114.55h48.5c5.96,0,11.24.86,15.82,2.58,4.58,1.72,8.43,4.07,11.52,7.05,3.1,2.98,5.42,6.54,6.97,10.66,1.55,4.13,2.32,8.66,2.32,13.59ZM740.52,345.29c0-6.88-2.04-12.18-6.11-15.91-4.07-3.72-9.83-5.59-17.29-5.59h-31.3v43.34h31.3c7.34,0,13.07-1.98,17.2-5.93s6.19-9.26,6.19-15.91Z"/>
<path class="cls-3" d="M832.36,311.41h16.68l42.66,114.55h-15.31l-10.66-30.1h-50.05l-10.49,30.1h-15.31l42.48-114.55ZM820.15,383.13h41.28l-20.64-59.68-20.64,59.68Z"/>
<path class="cls-3" d="M925.07,311.41h46.1c5.85,0,11.03.92,15.57,2.75,4.53,1.84,8.34,4.3,11.44,7.4s5.44,6.74,7.05,10.92c1.6,4.19,2.41,8.69,2.41,13.5s-.8,9.15-2.41,13.33c-1.61,4.19-3.96,7.86-7.05,11.01-3.1,3.16-6.91,5.65-11.44,7.48-4.53,1.84-9.72,2.75-15.57,2.75h-30.96v45.41h-15.14v-114.55ZM969.44,368.16c7.22,0,12.84-1.98,16.86-5.93,4.01-3.96,6.02-9.37,6.02-16.25s-2.01-12.3-6.02-16.25c-4.02-3.96-9.63-5.93-16.86-5.93h-29.24v44.38h29.24Z"/>
<path class="cls-3" d="M1043.06,311.41h15.14v49.36h58.31v-49.36h15.14v114.55h-15.14v-51.77h-58.31v51.77h-15.14v-114.55Z"/>
<path class="cls-3" d="M1176.53,311.41h15.14v114.55h-15.14v-114.55Z"/>
<path class="cls-3" d="M1245.67,368.68c0,14.68,3.9,26.03,11.7,34.06,7.8,8.03,18.75,12.04,32.85,12.04,5.16,0,9.86-.46,14.1-1.38,4.24-.92,8.2-2.06,11.87-3.44l2.41,12.21c-3.33,1.61-7.74,2.98-13.24,4.13-5.5,1.14-11.47,1.72-17.89,1.72-8.83,0-16.74-1.41-23.74-4.21-7-2.81-12.96-6.79-17.89-11.95-4.93-5.16-8.72-11.38-11.35-18.66-2.64-7.28-3.96-15.45-3.96-24.51s1.32-17.23,3.96-24.51c2.64-7.28,6.42-13.5,11.35-18.66,4.93-5.16,10.89-9.14,17.89-11.95,6.99-2.81,14.91-4.21,23.74-4.21,6.42,0,12.38.58,17.89,1.72,5.5,1.15,9.92,2.52,13.24,4.13l-2.41,12.21c-3.67-1.38-7.63-2.52-11.87-3.44-4.24-.92-8.94-1.38-14.1-1.38-14.1,0-25.06,4.02-32.85,12.04-7.8,8.03-11.7,19.38-11.7,34.06Z"/>
<path class="cls-3" d="M1432.46,395.34c0,10.32-3.7,18.35-11.09,24.08-7.4,5.74-17.57,8.6-30.53,8.6-6.88,0-13.82-.8-20.81-2.41-7-1.6-13.48-3.61-19.44-6.02l3.44-12.21c5.62,2.3,11.61,4.21,17.97,5.76,6.36,1.55,12.64,2.32,18.83,2.32,8.14,0,14.65-1.49,19.52-4.47,4.87-2.98,7.31-7.68,7.31-14.1s-2.47-11.09-7.4-14.36c-4.93-3.27-12.38-6.16-22.36-8.69-12.62-3.1-22.05-7.16-28.29-12.21-6.25-5.04-9.37-12.04-9.37-20.98,0-10.89,3.64-18.83,10.92-23.82,7.28-4.99,17.4-7.48,30.36-7.48,7.34,0,13.99.66,19.95,1.98,5.96,1.32,11.58,3.18,16.86,5.59l-2.92,12.21c-4.82-2.06-10.12-3.81-15.91-5.25-5.79-1.43-11.73-2.15-17.8-2.15-8.49,0-15.05,1.32-19.69,3.96-4.64,2.64-6.97,7.34-6.97,14.1,0,3.21.57,5.88,1.72,8,1.15,2.12,2.89,3.93,5.25,5.42,2.35,1.49,5.33,2.84,8.94,4.04,3.61,1.2,7.88,2.49,12.81,3.87,5.16,1.49,10.09,3.13,14.79,4.9,4.7,1.78,8.83,3.96,12.38,6.54,3.55,2.58,6.36,5.71,8.43,9.37,2.06,3.67,3.1,8.14,3.1,13.42Z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,39 @@
import { Meta } from '@storybook/blocks';
import { parameters } from '../../docs/utils/docsPage.js';
<Meta title="Actions/cssVariables" parameters={{ ...parameters }} />
# `cssVariables`
An action you can use to easily set [CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) on HTML elements. Useful for passing JavaScript values to your component SCSS like this:
```svelte
<script>
import { cssVariables } from '@reuters-graphics/graphics-components';
let { height = 300, textColour = 'red' } = $props();
// Create an object of variable names and CSS values...
let variables = $derived({
height: height + 'px',
textColour: textColour,
});
</script>
<!-- Attach it to a parent element with the action -->
<div use:cssVariables={variables}>
<p>My text...</p>
</div>
<style lang="scss">
/**
* Now use your variables in your SCSS!
*/
div {
height: var(--height);
p {
color: var(--textColour);
}
}
</style>
```

View file

@ -0,0 +1,20 @@
// Shamelessly stolen from: https://github.com/kaisermann/svelte-css-vars
export default (node: HTMLElement, props: Record<string, string>) => {
Object.entries(props).forEach(([key, value]) => {
node.style.setProperty(`--${key}`, value);
});
return {
update(newProps: Record<string, string>) {
Object.entries(newProps).forEach(([key, value]) => {
node.style.setProperty(`--${key}`, value);
delete props[key];
});
Object.keys(props).forEach((name) => {
node.style.removeProperty(`--${name}`);
});
props = newProps;
},
};
};

View file

@ -0,0 +1,25 @@
// Shamelessly stolen from https://github.com/sveltejs/svelte/issues/7583#issue-1260717165
let observer: ResizeObserver;
let callbacks: WeakMap<Element, (el: Element) => unknown>;
export default (element: HTMLElement, onResize: (el: Element) => unknown) => {
if (!observer) {
callbacks = new WeakMap();
observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const onResize = callbacks.get(entry.target);
if (onResize) onResize(entry.target);
}
});
}
callbacks.set(element, onResize);
observer.observe(element);
return {
destroy: () => {
callbacks.delete(element);
observer.unobserve(element);
},
};
};

View file

@ -0,0 +1,20 @@
import { Meta } from '@storybook/blocks';
import { parameters } from '../../docs/utils/docsPage.js';
<Meta title="Actions/resizeObserver" parameters={{ ...parameters }} />
# `resizeObserver`
An action you can use to easily to check when a DOM element's dimensions change using the [Resize Observer API](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver). Use it like this:
```svelte
<script>
import { resizeObserver } from '@reuters-graphics/graphics-components';
let elementWidth = 0;
</script>
<div use:resizeObserver={(element) => (elementWidth = element.clientWidth)}>
My width is: {elementWidth}
</div>
```

11
src/app.html Normal file
View file

@ -0,0 +1,11 @@
<!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>

View file

@ -0,0 +1,128 @@
import type { Component } from 'svelte';
import type { TransitionOptions } from '../ScrollerVideo/ts/ScrollerVideo.js';
import type { ScrollerVideoState } from '../ScrollerVideo/ts/state.svelte.js';
/**
* Used for the list of <option> tags nested in a <select> input.
*/
export type Option = {
value: string;
text: string;
};
/**
* Used for any props that restrict width of a container to one of pre-fab widths.
*/
export type ContainerWidth =
| 'narrower'
| 'narrow'
| 'normal'
| 'wide'
| 'wider'
| 'widest'
| 'fluid';
/**
* Used to set headline class fluid size from text-2xl to text-6xl
*/
export type HeadlineSize = 'small' | 'normal' | 'big' | 'bigger' | 'biggest';
/**
* A step in the Scroller component.
*/
export interface ScrollerStep {
/**
* A background component
*/
background: Component | undefined;
/**
* Optional props for background component
*/
backgroundProps?: object;
/**
* A component or markdown-formatted string
*/
foreground: Component | string;
/**
* Optional props for foreground component
*/
foregroundProps?: object;
/**
* Optional alt text for the background, read aloud after the foreground text. You can add it to each step or just to the first step to describe the entire scroller graphic.
*/
altText?: string;
}
export type ForegroundPosition =
| 'middle'
| 'left'
| 'right'
| 'left opposite'
| 'right opposite';
export type ScrollerVideoForegroundPosition =
| 'top center'
| 'top left'
| 'top right'
| 'bottom center'
| 'bottom left'
| 'bottom right'
| 'center center'
| 'center left'
| 'center right';
export type LottieForegroundPosition =
| 'top center'
| 'top left'
| 'top right'
| 'bottom center'
| 'bottom left'
| 'bottom right'
| 'center center'
| 'center left'
| 'center right';
// Complete ScrollerVideo instance interface
export interface ScrollerVideoInstance {
// Properties
container: HTMLElement | null;
scrollerVideoContainer: Element | string | undefined;
src: string;
transitionSpeed: number;
frameThreshold: number;
useWebCodecs: boolean;
objectFit: string;
sticky: boolean;
trackScroll: boolean;
onReady: () => void;
onChange: (percentage?: number) => void;
debug: boolean;
autoplay: boolean;
video: HTMLVideoElement | undefined;
videoPercentage: number;
isSafari: boolean;
currentTime: number;
targetTime: number;
canvas: HTMLCanvasElement | null;
context: CanvasRenderingContext2D | null;
frames: ImageBitmap[] | null;
frameRate: number;
targetScrollPosition: number | null;
currentFrame: number;
usingWebCodecs: boolean;
totalTime: number;
transitioningRaf: number | null;
componentState: ScrollerVideoState;
// Methods
updateScrollPercentage: ((jump: boolean) => void) | undefined;
resize: (() => void) | undefined;
setVideoPercentage(percentage: number, options?: TransitionOptions): void;
setCoverStyle(el: HTMLElement | HTMLCanvasElement | undefined): void;
decodeVideo(): Promise<void>;
paintCanvasFrame(frameNum: number): void;
transitionToTargetTime(options: TransitionOptions): void;
setTargetTimePercent(percentage: number, options?: TransitionOptions): void;
setScrollPercent(percentage: number): void;
destroy(): void;
autoplayScroll(): void;
updateDebugInfo(): void;
}

View file

@ -0,0 +1,52 @@
export type LeaderboardAdType = {
mobile: {
adType: 'leaderboard';
placementName: 'reuters_mobile_leaderboard';
};
desktop: {
adType: 'leaderboard';
placementName: 'reuters_desktop_leaderboard_atf';
};
};
export type SponsorshipAdType = {
mobile: {
adType: 'sponsorlogo';
placementName: 'reuters_sponsorlogo';
};
desktop: {
adType: 'sponsorlogo';
placementName: 'reuters_sponsorlogo';
};
};
export type InlineAdType = {
mobile: {
adType: 'mpu' | 'native' | 'mpu2';
placementName:
| 'reuters_mobile_mpu_1'
| 'reuters_mobile_mpu_2'
| 'reuters_mobile_mpu_3';
};
desktop: {
adType: 'native' | 'canvas' | 'flex';
placementName:
| 'reuters_desktop_native_1'
| 'reuters_desktop_native_2'
| 'reuters_desktop_native_3';
};
};
export type DesktopPlacementName =
| LeaderboardAdType['desktop']['placementName']
| SponsorshipAdType['desktop']['placementName']
| InlineAdType['desktop']['placementName'];
export type MobilePlacementName =
| LeaderboardAdType['mobile']['placementName']
| SponsorshipAdType['mobile']['placementName']
| InlineAdType['mobile']['placementName'];
export type DesktopAdType =
| LeaderboardAdType['desktop']['adType']
| SponsorshipAdType['desktop']['adType']
| InlineAdType['desktop']['adType'];
export type MobileAdType =
| LeaderboardAdType['mobile']['adType']
| SponsorshipAdType['mobile']['adType']
| InlineAdType['mobile']['adType'];

View file

@ -0,0 +1,35 @@
<script lang="ts">
import { onMount } from 'svelte';
import { loadBootstrap } from './adScripts/bootstrap';
import { loadScript } from './adScripts/loadScript';
import OneTrust from './OneTrust.svelte';
onMount(() => {
window.graphicsAdQueue = window.graphicsAdQueue || [];
loadScript(
'https://www.reuters.com/static/js/bootstrap/v1.1.2/bootstrap.static.js',
{ onload: loadBootstrap, async: false }
);
// Load Freestar script
loadScript('https://a.pub.network/reuters-com/pubfig.min.js');
});
</script>
<svelte:head>
<link rel="preconnect" href="https://a.pub.network/" crossorigin="" />
<link rel="preconnect" href="https://b.pub.network/" crossorigin="" />
<link rel="preconnect" href="https://c.pub.network/" crossorigin="" />
<link rel="preconnect" href="https://d.pub.network/" crossorigin="" />
<link rel="preconnect" href="https://c.amazon-adsystem.com" crossorigin="" />
<link rel="preconnect" href="https://s.amazon-adsystem.com" crossorigin="" />
<link rel="preconnect" href="https://btloader.com/" crossorigin="" />
<link rel="preconnect" href="https://api.btloader.com/" crossorigin="" />
<link
rel="preconnect"
href="https://confiant-integrations.global.ssl.fastly.net"
crossorigin=""
/>
<link rel="stylesheet" href="https://a.pub.network/reuters-com/cls.css" />
</svelte:head>
<OneTrust />

View file

@ -0,0 +1,64 @@
<script lang="ts">
import type {
DesktopAdType,
DesktopPlacementName,
MobileAdType,
MobilePlacementName,
} from './@types/ads';
import { onMount } from 'svelte';
import { getRandomAdId } from './utils';
interface Props {
placementName: DesktopPlacementName | MobilePlacementName;
adType: DesktopAdType | MobileAdType;
/**
* @TODO Unclear at what level this bit of config is used with placements...
*/
dataFreestarAd?: string;
}
let { placementName, adType, dataFreestarAd = '__970x250' }: Props = $props();
const adId = getRandomAdId();
onMount(() => {
const adSlot = {
placementName,
slotId: adId,
targeting: {
div_id: placementName,
type: adType,
},
};
// @ts-ignore window global
const freestar = window?.freestar;
// Add adSlot to freestar queue directly if already initialised
if (freestar) {
freestar.queue.push(function () {
freestar.newAdSlots([adSlot], freestar.config.channel);
});
// ... otherwise add to the graphicsAdQueue queue.
} else {
window.graphicsAdQueue = window.graphicsAdQueue || [];
window.graphicsAdQueue.push(adSlot);
}
return () => {
// @ts-ignore window global
const freestar = window?.freestar;
if (freestar) {
freestar.queue.push(function () {
freestar.deleteAdSlots(adId);
});
}
};
});
</script>
<div data-freestar-ad={dataFreestarAd || null} id={adId}></div>
<style>
:global(div.freestar-adslot:has(.unfulfilled-ad)) {
display: none;
}
</style>

View file

@ -0,0 +1,56 @@
import { Meta, Canvas } from '@storybook/blocks';
import * as InlineAdStories from './InlineAd.stories.svelte';
<Meta of={InlineAdStories} />
# InlineAd
Add programmatic ads inline on your page.
> **IMPORTANT!** Make sure ads are only used on dotcom pages, never on embeds.
```svelte
<!-- +page.svelte -->
<script>
import { AdScripts } from '@reuters-graphics/graphics-components';
</script>
<!-- Include AdScripts only ONCE per page for any type of ad -->
<AdScripts />
```
```svelte
<!-- App.svelte -->
<script>
import { InlineAd } from '@reuters-graphics/graphics-components';
let { embedded = false } = $props();
</script>
{#each content.blocks as block}
<!-- ... -->
{#if block.Type === 'inline-ad'}
<!-- Check if in an embed context! -->
{#if !embedded}
<InlineAd />
{/if}
{/if}
<!-- ... -->
{/each}
```
You may add **up to three** inline ads per page, but must set the `n` prop on multiple ads in sequential order, 1 - 3.
```svelte
<!-- First inline ad on the page -->
<InlineAd n={1} />
<!-- ... second ... -->
<InlineAd n={2} />
<!-- ... third and final. -->
<InlineAd n={3} />
```
<Canvas of={InlineAdStories.Demo} />

View file

@ -0,0 +1,20 @@
<script module lang="ts">
import AdScripts from './AdScripts.svelte';
import InlineAd from './InlineAd.svelte';
import { defineMeta } from '@storybook/addon-svelte-csf';
const { Story } = defineMeta({
title: 'Components/Ads & analytics/InlineAd',
component: InlineAd,
});
</script>
{#snippet template()}
<div>
<AdScripts />
<InlineAd />
<InlineAd />
</div>
{/snippet}
<Story name="Demo" children={template} />

View file

@ -0,0 +1,74 @@
<!-- @migration-task Error while migrating Svelte code: Cannot set properties of undefined (setting 'next') -->
<!-- @component `InlineAd` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-ads-analytics-inlinead--docs) -->
<script lang="ts">
import Block from '../Block/Block.svelte';
import type { InlineAdType } from './@types/ads';
import ResponsiveAd from './ResponsiveAd.svelte';
interface Props {
/** Add an ID to target with SCSS. */
id?: string;
/** Number of the inline ad in sequence. Use to add multiple inline ads to a page. */
n?: 1 | 2 | 3 | '1' | '2' | '3';
/** Add a class to target with SCSS. Defaults to `my-12`. */
class?: string;
}
let { id = '', class: cls = 'my-12', n = 1 }: Props = $props();
const desktopPlacementName: InlineAdType['desktop']['placementName'] = `reuters_desktop_native_${n}`;
</script>
<Block {id} class="freestar-adslot {cls}">
<div class="ad-block">
<div class="ad-label">Advertisement · Scroll to continue</div>
<div class="ad-container">
<div class="ad-slot__inner">
<div>
<ResponsiveAd {desktopPlacementName} />
</div>
</div>
</div>
</div>
</Block>
<style lang="scss">
div.ad-block {
border-bottom: 1px solid var(--theme-colour-brand-rules);
border-top: 1px solid var(--theme-colour-brand-rules);
div.ad-label {
font-family: Knowledge, 'Source Sans Pro', Arial, Helvetica, sans-serif;
font-size: 14px;
margin: 6px 0;
line-height: 1.333;
color: var(--theme-colour-text-secondary);
width: 100%;
text-align: center;
}
div.ad-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 415px;
@media (max-width: 767.9px) {
min-height: 320px;
}
div.ad-slot__inner {
margin: auto 0;
width: 100%;
max-width: 100%;
flex: unset;
& > div {
display: block;
:global(div[data-freestar-ad]) {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
}
}
}
}
</style>

View file

@ -0,0 +1,31 @@
import { Meta, Canvas } from '@storybook/blocks';
import * as LeaderboardAdStories from './LeaderboardAd.stories.svelte';
<Meta of={LeaderboardAdStories} />
# LeaderboardAd
Add a leaderboard ad to your page.
> **IMPORTANT!** Make sure ads are only used on dotcom pages, never on embeds.
```svelte
<!-- +page.svelte -->
<script>
import {
AdScripts,
LeaderboardAd,
SiteHeader,
} from '@reuters-graphics/graphics-components';
</script>
<!-- Include AdScripts only ONCE per page for any type of ad -->
<AdScripts />
<!-- ALWAYS put the leaderboard ad directly above the SiteHeader -->
<LeaderboardAd />
<SiteHeader />
```
<Canvas of={LeaderboardAdStories.Demo} />

View file

@ -0,0 +1,29 @@
<script module lang="ts">
import AdScripts from './AdScripts.svelte';
import LeaderboardAd from './LeaderboardAd.svelte';
import { defineMeta } from '@storybook/addon-svelte-csf';
const { Story } = defineMeta({
title: 'Components/Ads & analytics/LeaderboardAd',
component: LeaderboardAd,
});
</script>
{#snippet template()}
<div>
<AdScripts />
<LeaderboardAd />
</div>
{/snippet}
<Story name="Demo" children={template} />
<style>
div {
min-height: 200vh;
background-size: 40px 40px;
background-image:
linear-gradient(to right, lightgrey 1px, transparent 1px),
linear-gradient(to bottom, lightgrey 1px, transparent 1px);
}
</style>

View file

@ -0,0 +1,102 @@
<!-- @component `LeaderboardAd` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-ads-analytics-leaderboardad--docs) -->
<script lang="ts">
import type { LeaderboardAdType } from './@types/ads';
import ResponsiveAd from './ResponsiveAd.svelte';
import { onMount } from 'svelte';
interface Props {
/** Add an ID to target with SCSS. */
id?: string;
/** Add a class to target with SCSS. */
class?: string;
}
let { id = '', class: cls = '' }: Props = $props();
let windowWidth = $state(1200);
let adSize = $derived(windowWidth < 1024 ? 110 : 275);
const desktopPlacementName: LeaderboardAdType['desktop']['placementName'] =
'reuters_desktop_leaderboard_atf';
let sticky = $state(false);
// Handles transition out... somewhat dumb, but here we are...
let unstick = $state(false);
onMount(() => {
const handleScroll = () => {
const scrollTop = window.scrollY;
if (scrollTop >= adSize * 1.1) {
sticky = true;
setTimeout(() => {
unstick = true;
setTimeout(() => {
sticky = false;
}, 400);
}, 1500);
window.removeEventListener('scroll', handleScroll);
}
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
});
</script>
<svelte:window bind:innerWidth={windowWidth} />
<div
class="freestar-adslot leaderboard__sticky {cls}"
class:sticky
class:unstick
{id}
style="--height: {adSize}px;"
>
<div class="ad-block">
<div class="ad-slot__container">
<div class="ad-slot__inner">
<div>
<ResponsiveAd {desktopPlacementName} />
</div>
</div>
</div>
</div>
</div>
<style lang="scss">
.leaderboard__sticky {
position: initial;
top: -275px;
transition: top 0.4s ease-in-out;
z-index: 1030;
&.sticky {
position: sticky;
top: 0px;
}
&.unstick {
top: -275px;
}
}
div.ad-block {
width: 100%;
background: #f4f4f4;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
height: var(--height);
.ad-slot__container {
height: 0px;
min-height: var(--height);
align-items: center;
display: flex;
justify-content: center;
.ad-slot__inner {
max-width: 100%;
}
}
}
</style>

View file

@ -0,0 +1,44 @@
<!-- This component manages the OneTrust prefs button, so it's not permanently fixed on page... -->
<script lang="ts">
import { onMount } from 'svelte';
import { throttle } from 'es-toolkit';
let lastScroll = 0;
let showManagePreferences = true;
const togglePrefs = (on: boolean = true) => {
const btn = document.getElementById('ot-sdk-btn-floating');
if (!btn) return;
if (on) {
showManagePreferences = true;
btn.style.bottom = '';
} else {
showManagePreferences = false;
btn.style.bottom = '-5rem';
}
};
const handleScroll = () => {
if (lastScroll > window.scrollY) {
if (!showManagePreferences) {
togglePrefs(true);
}
} else {
if (showManagePreferences && window.scrollY > 250) {
togglePrefs(false);
}
}
lastScroll = window.scrollY;
};
onMount(() => {
if (typeof window === 'undefined') return;
const throttledHandle = throttle(handleScroll, 250);
window.addEventListener('scroll', throttledHandle, {
passive: true,
});
return () => {
window.removeEventListener('scroll', throttledHandle);
};
});
</script>

View file

@ -0,0 +1,78 @@
<script lang="ts">
import type {
DesktopAdType,
DesktopPlacementName,
MobileAdType,
MobilePlacementName,
} from './@types/ads';
import AdSlot from './AdSlot.svelte';
interface Props {
desktopPlacementName: DesktopPlacementName;
mobileBreakpoint?: number;
}
let { desktopPlacementName, mobileBreakpoint = 1024 }: Props = $props();
let windowWidth: number | undefined = $state();
const getMobilePlacementName = (
desktopPlacementName: DesktopPlacementName
): MobilePlacementName => {
switch (desktopPlacementName) {
case 'reuters_desktop_leaderboard_atf':
return 'reuters_mobile_leaderboard';
case 'reuters_sponsorlogo':
return 'reuters_sponsorlogo';
case 'reuters_desktop_native_1':
return 'reuters_mobile_mpu_1';
case 'reuters_desktop_native_2':
return 'reuters_mobile_mpu_2';
case 'reuters_desktop_native_3':
return 'reuters_mobile_mpu_3';
default:
return 'reuters_mobile_mpu_1';
}
};
const getAdType = (
placementName: DesktopPlacementName | MobilePlacementName
): DesktopAdType | MobileAdType => {
switch (placementName) {
case 'reuters_desktop_leaderboard_atf':
case 'reuters_mobile_leaderboard':
return 'leaderboard';
case 'reuters_sponsorlogo':
return 'sponsorlogo';
case 'reuters_mobile_mpu_1':
return 'mpu';
case 'reuters_mobile_mpu_2':
return 'native';
case 'reuters_mobile_mpu_3':
return 'mpu2';
case 'reuters_desktop_native_1':
return 'native';
case 'reuters_desktop_native_2':
return 'canvas';
case 'reuters_desktop_native_3':
return 'flex';
default:
return 'native';
}
};
let placementName = $derived(
windowWidth && windowWidth < mobileBreakpoint ?
getMobilePlacementName(desktopPlacementName)
: desktopPlacementName
);
let adType = $derived(getAdType(placementName));
</script>
<svelte:window bind:innerWidth={windowWidth} />
{#if windowWidth}
{#key placementName}
<AdSlot {placementName} {adType} />
{/key}
{/if}

View file

@ -0,0 +1,37 @@
import { Meta, Canvas } from '@storybook/blocks';
import * as SponsorshipAdStories from './SponsorshipAd.stories.svelte';
<Meta of={SponsorshipAdStories} />
# SponsorshipAd
Add a sponsorship ad to your page.
> **IMPORTANT!** Make sure ads are only used on dotcom pages, never on embeds.
```svelte
<!-- +page.svelte -->
<script>
import { AdScripts } from '@reuters-graphics/graphics-components';
</script>
<!-- Include AdScripts only ONCE per page for any type of ad -->
<AdScripts />
```
```svelte
<!-- App.svelte -->
<script>
import { SponsorshipAd } from '@reuters-graphics/graphics-components';
let { embedded = false } = $props();
</script>
<!-- Check if in an embed context! -->
{#if !embedded}
<SponsorshipAd />
{/if}
```
<Canvas of={SponsorshipAdStories.Demo} />

View file

@ -0,0 +1,20 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import AdScripts from './AdScripts.svelte';
import SponsorshipAd from './SponsorshipAd.svelte';
const { Story } = defineMeta({
title: 'Components/Ads & analytics/SponsorshipAd',
component: SponsorshipAd,
});
</script>
{#snippet template()}
<div>
<AdScripts />
<SponsorshipAd />
</div>
{/snippet}
<Story name="Demo" children={template} />

View file

@ -0,0 +1,85 @@
<!-- @migration-task Error while migrating Svelte code: Cannot set properties of undefined (setting 'next') -->
<!-- @component `SponsorshipAd` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-ads-analytics-sponsorshipad--docs) -->
<script lang="ts">
import Block from '../Block/Block.svelte';
import type { SponsorshipAdType } from './@types/ads';
import ResponsiveAd from './ResponsiveAd.svelte';
interface Props {
/** Add an ID to target with SCSS. */
id?: string;
/** Add a class to target with SCSS. */
class?: string;
/**
* Label placed directly above the sponsorship ad
*/
adLabel?: string;
}
let { id = '', class: cls = 'my-12', adLabel = '' }: Props = $props();
const desktopPlacementName: SponsorshipAdType['desktop']['placementName'] =
'reuters_sponsorlogo';
</script>
<Block {id} class="freestar-adslot {cls}">
<div class="ad-block">
{#if adLabel}
<div class="ad-label">
<div>{adLabel}</div>
</div>
{/if}
<div class="ad-container">
<div class="ad-slot__inner">
<div>
<ResponsiveAd {desktopPlacementName} />
</div>
</div>
</div>
</div>
</Block>
<style lang="scss">
div.ad-block {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
gap: 10px;
div.ad-label {
font-family: Knowledge, 'Source Sans Pro', Arial, Helvetica, sans-serif;
font-size: 12px;
margin: 0;
line-height: 1.333;
color: var(--theme-colour-text-secondary);
text-align: right;
display: flex;
align-items: center;
justify-content: center;
justify-items: center;
}
div.ad-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
min-height: 32px;
div.ad-slot__inner {
margin: auto 0;
width: 100%;
max-width: 100%;
flex: unset;
& > div {
display: block;
:global(div[data-freestar-ad]) {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
}
}
}
}
</style>

View file

@ -0,0 +1,105 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import getParameterByName from './getParameterByName';
import Ias from './ias';
const ONETRUST_LOGS = 'ot_logs';
const ONETRUST_GEOLOCATION_MOCK = 'ot_geolocation_mock';
const ONETRUST_SCRIPT_ID = '38cb75bd-fbe1-4ac8-b4af-e531ab368caf';
export const loadBootstrap = () => {
(<any>window).freestar = (<any>window).freestar || {};
const freestar = (<any>window).freestar;
freestar.debug = true;
freestar.queue = freestar.queue || [];
freestar.config = freestar.config || {};
freestar.config.enabled_slots = [];
freestar.initCallback = function () {
if (freestar.config.enabled_slots.length === 0) {
freestar.initCallbackCalled = false;
} else {
freestar.newAdSlots(freestar.config.enabled_slots);
}
};
freestar.config.channel = '/4735792/reuters.com/graphics';
(<any>window).initBootstrap(
{
onetrust_logs: getParameterByName(ONETRUST_LOGS) || 'false',
geolocation_mock:
getParameterByName(ONETRUST_GEOLOCATION_MOCK) || 'default',
onetrust_script_id: ONETRUST_SCRIPT_ID,
},
(onetrustResponse: any) => {
const iasPromise = Ias();
return Promise.all([iasPromise]).then((responses) => {
const [iasResponse] = responses;
return {
...onetrustResponse,
ias: iasResponse,
};
});
}
);
(<any>window).bootstrap.getResults(() => {
// Set GAM
window.googletag = (<any>window).googletag || { cmd: [] };
window.googletag.cmd.push(() => {
window.googletag.pubads().enableSingleRequest();
/**
* @TODO Property 'enableAsyncRendering' does not exist on type 'PubAdsService'.
*/
// @ts-ignore window global
window.googletag.pubads().enableAsyncRendering();
window.googletag.pubads().collapseEmptyDivs(true);
window.googletag
.pubads()
.addEventListener('slotRenderEnded', function (event) {
const adDiv = document.getElementById(event.slot.getSlotElementId());
if (!adDiv) return;
// If the ad slot is empty
if (event.isEmpty) {
adDiv.classList.add('unfulfilled-ad');
} else {
adDiv.classList.remove('unfulfilled-ad');
}
});
});
// Set page-level key-values
// cf: https://help.freestar.com/help/using-key-values
freestar.queue.push(function () {
// Global Ads test targeting
const adstest = new URL(document.location.href).searchParams.get(
'adstest'
);
if (adstest) {
window.googletag.pubads().setTargeting('adstest', adstest);
}
// Use the URL path to create a unique ID for the page.
const graphicId =
window.location.pathname
.split('/')
// Get the first lowercase slug in the pathname, which is the graphic UID.
.filter((d) => d.match(/[a-z0-9]+/) && d !== 'graphics')[0] ||
'unknown-graphic';
window.googletag.pubads().setTargeting('template', 'graphics');
window.googletag.pubads().setTargeting('graphicId', graphicId);
});
if (!Array.isArray((<any>window).graphicsAdQueue)) {
console.error('Ad queue not initialized!');
}
freestar.queue.push(function () {
freestar.newAdSlots(
(<any>window).graphicsAdQueue || [],
freestar.config.channel
);
});
});
};

View file

@ -0,0 +1,12 @@
export default (name: string, url = window.location.href) => {
// eslint-disable-next-line no-useless-escape
name = name.replace(/[\[\]]/g, '\\$&');
const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)');
const results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, ' '));
};

View file

@ -0,0 +1,28 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
const IAS_REQUEST_TIMEOUT = 600;
export default () => {
return new Promise((resolve) => {
const timerId = setTimeout(() => {
resolve('Resolved with timeout');
}, IAS_REQUEST_TIMEOUT);
const setupIAS = () => {
clearTimeout(timerId);
(<any>window).__iasPET = (<any>window).__iasPET || {};
(<any>window).__iasPET.queue = (<any>window).__iasPET.queue || [];
(<any>window).__iasPET.pubId = '931336'; // Ask Rachel
resolve('loaded');
};
// Set up IAS pet.js
const script = document.createElement('script');
script.src = '//static.adsafeprotected.com/iasPET.1.js';
script.setAttribute('async', 'async');
document.head.appendChild(script);
script.onload = setupIAS;
script.onerror = () => {
resolve('error');
};
});
};

View file

@ -0,0 +1,17 @@
interface attributesInterface {
onload?: () => void;
async?: boolean;
}
export const loadScript = (src: string, attributes?: attributesInterface) => {
const { onload, async = true } = attributes || {};
const existingScript = document.querySelector(`script[src="${src}"]`);
if (existingScript) return;
const script = document.createElement('script');
if (onload) script.addEventListener('load', onload);
script.async = async;
script.src = src;
document.head.append(script);
};

View file

@ -0,0 +1,6 @@
const random4 = () =>
Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
export const getRandomAdId = () => 'ad-' + random4() + random4();

View file

@ -0,0 +1,73 @@
import { Meta } from '@storybook/blocks';
import * as AnalyticsStories from './Analytics.stories.svelte';
<Meta of={AnalyticsStories} />
# Analytics
The `Analytics` component adds Google and Chartbeat analytics to your page.
```svelte
<script>
import { Analytics } from '@reuters-graphics/graphics-components';
const authors = [{ name: 'Jane Doe' }, { name: 'John Doe' }];
</script>
<Analytics {authors} />
```
## Environments
Generally, you only want to send page analytics in production environments.
In a SvelteKit context, you can use `$app` stores to restrict when you send analytics.
For example, the following excludes analytics from pages in development or hosted on our preview server:
```svelte
<script>
import { Analytics } from '@reuters-graphics/graphics-components';
import { dev } from '$app/environment';
import { page } from '$app/stores';
</script>
{#if !dev && $page.url?.hostname !== 'graphics.thomsonreuters.com'}
<Analytics />
{/if}
```
## Multipage apps
If you're using analytics to measure a multipage newsapp that uses [client-side routing](https://kit.svelte.dev/docs/glossary#routing), then you may need to trigger analytics after virtual page navigation.
This component exports a function you can call to register pageviews.
For example, here's how you can use SvelteKit's [`afterNavigate`](https://kit.svelte.dev/docs/modules#$app-navigation-afternavigate) lifecycle to capture additional pageviews:
```svelte
<script>
import {
Analytics,
registerPageview,
} from '@reuters-graphics/graphics-components';
import { afterNavigate } from '$app/navigation';
let isFirstPageview = true;
afterNavigate(() => {
// We shouldn't fire on initial page load because the Analytics component
// already registers a reader's first pageview. After this component
// has initially mounted, we can be sure that further navigation is virtual
// and register pageviews using this function.
if (!isFirstPageview) {
registerPageview();
} else {
isFirstPageview = false;
}
});
</script>
<Analytics />
```

View file

@ -0,0 +1,17 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import Analytics from './Analytics.svelte';
const { Story } = defineMeta({
title: 'Components/Ads & analytics/Analytics',
component: Analytics,
});
</script>
<Story
name="Demo"
tags={['!autodocs', '!dev']}
args={{
authors: [{ name: 'Jane Doe' }, { name: 'John Doe' }],
}}
/>

View file

@ -0,0 +1,39 @@
<!-- @component `Analytics` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-ads-analytics-analytics--docs) -->
<script module>
import { registerPageview as registerChartbeatPageview } from './providers/chartbeat';
import { registerPageview as registerGAPageview } from './providers/ga';
/** Register virtual pageviews when using client-side routing in multipage applications. */
function registerPageview() {
registerChartbeatPageview();
registerGAPageview();
}
export { registerPageview };
</script>
<script lang="ts">
interface Author {
name: string;
}
import { onMount } from 'svelte';
import { ga, chartbeat } from './providers';
import GoogleTagManager from './GTM.svelte';
interface Props {
/**
* Used to associate a page with its author(s) in Chartbeat.
*/
authors?: Author[];
}
let { authors = [] }: Props = $props();
onMount(() => {
ga();
chartbeat(authors);
});
</script>
<GoogleTagManager />

View file

@ -0,0 +1,30 @@
<script lang="ts">
const GTM_ID = 'GTM-P9TTSWG2';
</script>
<svelte:head>
<!-- Google Tag Manager -->
<link href="https://www.googletagmanager.com" rel="preconnect" />
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'gtm.start': new Date().getTime(),
event: 'gtm.js',
});
</script>
<script async src={`https://www.googletagmanager.com/gtm.js?id=${GTM_ID}`}>
</script>
<!-- End Google Tag Manager -->
</svelte:head>
<!-- Google Tag Manager (noscript) -->
<noscript>
<iframe
src={`https://www.googletagmanager.com/ns.html?id=${GTM_ID}`}
height="0"
width="0"
style="display:none;visibility:hidden"
title=""
></iframe>
</noscript>
<!-- End Google Tag Manager (noscript) -->

View file

@ -0,0 +1,23 @@
// Reuters Chartbeat UID
const UID = 52639;
export default (authors: { name: string }[]) => {
window._sf_async_config = {
uid: UID,
domain: 'reuters.com',
flickerControl: false,
useCanonical: true,
useCanonicalDomain: true,
sections: 'Graphics',
authors: authors.map((a) => a?.name || '').join(','),
...(window._sf_async_config || {}),
};
};
export const registerPageview = () => {
if (typeof window === 'undefined' || !window.pSUPERFLY) return;
window.pSUPERFLY.virtualPage({
path: window.location.pathname,
title: document?.title,
});
};

View file

@ -0,0 +1,24 @@
export default () => {
try {
window.dataLayer = window.dataLayer || [];
if (!window.gtag) {
/** @type {Gtag.Gtag} */
window.gtag = function () {
// eslint-disable-next-line prefer-rest-params
window.dataLayer.push(arguments);
};
window.gtag('js', new Date());
registerPageview();
}
} catch (e) {
console.warn(`Error initialising Google Analytics: ${e}`);
}
};
export const registerPageview = () => {
if (typeof window === 'undefined' || !window.gtag) return;
window.gtag('event', 'page_view', {
page_location: window.location.origin + window.location.pathname,
page_title: document?.title,
});
};

View file

@ -0,0 +1,2 @@
export { default as ga } from './ga';
export { default as chartbeat } from './chartbeat';

View file

@ -0,0 +1,117 @@
import { Meta, Canvas } from '@storybook/blocks';
import * as ArticleStories from './Article.stories.svelte';
<Meta of={ArticleStories} />
# Article
The `Article` component contains all the contents of our story.
> 📌 In most cases, **you don't need to mess with the `Article` component** because it's already set up in the graphics kit.
```svelte
<script>
import { Article } from '@reuters-graphics/graphics-components';
</script>
<Article>
<!-- The story content goes here! -->
</Article>
```
<Canvas of={ArticleStories.Demo} />
## Custom column widths
The `Article` component also establishes the widths of columns that contain individual sections of the story, such as text, photos, and charts. The default column widths follow a basic class scheme:
- `narrower` The narrowest...
- `narrow` A bit narrower than the default body text column
- `normal` **The default width of the body text column**
- `wide` A bit wider
- `wider` A bit wider than wide...
- `widest` Edge-to-edge, but _excluding_ the left and right padding on `Article`
- `fluid` Fully edge-to-edge
You can set custom column widths by passing an object to the `columnWidths` prop with pixel values for the `narrower`, `narrow`, `normal`, `wide` and `wider` classes. These can then be used by the `Block` component or other elements housed inside `<Article>`.
> **For most graphics kit pages, you shouldn't customise the column widths.** Other Reuters tools, like our AI templates, use our default column widths, so customising those widths here has downstream consequences for graphics made outside graphics kit. The main exception is SREP stories.
```svelte
<!-- Set custom column widths -->
<Article
columnWidths={{
narrower: 310,
narrow: 450,
normal: 550,
wide: 675,
wider: 1400,
}}
>
<!-- Custom column widths get passed down to the `Block` component -->
<Block width="narrower" />
<Block width="narrow" />
<Block width="normal" />
<Block width="wide" />
<Block width="wider" />
<Block width="widest" />
<Block width="fluid" />
</Article>
```
If you're not using our `Block` component, you can still inherit the column widths from `Article` and create your own custom containers by using [CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) like this:
```svelte
<div class="my-special-container">
<!-- Story content -->
</div>
<style lang="scss">
div.my-special-container {
max-width: var(--wide-column-width);
}
</style>
```
... or you can make your column widths entirely configurable by adding classes and manually specifying widths:
```svelte
<script>
let { width = 'normal' } = $props();
</script>
<div class="my-special-container {width}">
<!-- Story content -->
</div>
<style lang="scss">
div.my-special-container {
max-width: var(--normal-column-width);
&.narrower {
max-width: var(--narrower-column-width);
}
&.narrow {
max-width: var(--narrow-column-width);
}
&.wide {
max-width: var(--wide-column-width);
}
&.wider {
max-width: var(--wider-column-width);
}
&.widest {
max-width: 100%;
}
&.fluid {
width: calc(100% + 30px);
margin-inline-start: -15px;
max-width: none;
}
}
</style>
```
Here's an example of how <span className='custom'>custom</span> `columnWidths` can be used to change column widths:
<Canvas of={ArticleStories.CustomColumns} />

View file

@ -0,0 +1,117 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import Block from '../Block/Block.svelte';
import Article from './Article.svelte';
const { Story } = defineMeta({
title: 'Components/Page layout/Article',
component: Article,
});
</script>
<Story name="Demo">
<Article id="article-story-basic">
<div class="demo-container">
<div class="background-label">Article well</div>
<div class="padding-label"><span></span>15px padding</div>
</div>
</Article>
</Story>
<Story name="Custom columns" exportName="CustomColumns">
<h3>Default column widths</h3>
<Article id="article-column-widths-demo">
<div class="article-boundaries">
<Block id="section-demo" width="narrower">narrower</Block>
<Block id="section-demo" width="narrow">narrow</Block>
<Block id="section-demo">normal</Block>
<Block id="section-demo" width="wide">wide</Block>
<Block id="section-demo" width="wider">wider</Block>
<Block id="section-demo" width="widest">widest</Block>
<Block id="section-demo" width="fluid">fluid</Block>
</div>
</Article>
<h3>Custom column widths</h3>
<Article
id="article-column-widths-demo"
columnWidths={{
narrower: 250,
narrow: 400,
normal: 500,
wide: 675,
wider: 1400,
}}
>
<div class="article-boundaries custom">
<Block id="section-demo" width="narrower">narrower</Block>
<Block id="section-demo" width="narrow">narrow</Block>
<Block id="section-demo">normal</Block>
<Block id="section-demo" width="wide">wide</Block>
<Block id="section-demo" width="wider">wider</Block>
<Block id="section-demo" width="widest">widest</Block>
<Block id="section-demo" width="fluid">fluid</Block>
</div>
</Article>
</Story>
<style lang="scss">
h3 {
text-align: center;
}
:global(span.custom) {
color: rgb(211, 132, 123);
font-weight: 600;
}
:global(#article-story-basic, #article-column-widths-demo) {
width: calc(100% + 30px);
margin-inline-start: -15px;
}
:global(#article-column-widths-demo) {
background-color: #ddd;
position: relative;
margin-block-end: 10px;
}
:global(#article-column-widths-demo .article-boundaries) {
padding: 0;
width: 100%;
height: 100%;
background-color: #bbb;
}
:global(
#article-column-widths-demo .article-boundaries.custom div.article-block
) {
background: rgb(211, 132, 123);
}
:global(#article-column-widths-demo div.article-block) {
height: 300px;
background: #81a1c1;
margin-block-end: 2px;
height: 50px;
padding-inline-start: 3px;
color: white;
font-size: 1rem;
}
div.demo-container {
height: 300px;
background: #ccc;
position: relative;
.background-label {
font-size: 1.5rem;
position: absolute;
top: 40%;
left: 40%;
color: #666;
}
.padding-label {
font-size: 1rem;
position: absolute;
top: 0;
left: -17px;
span {
font-size: 18px;
}
}
}
</style>

View file

@ -0,0 +1,70 @@
<!-- @component `Article` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-page-layout-article--docs) -->
<script lang="ts">
interface ColumnWidths {
/** Narrower column width */
narrower: number;
/** Narrow column width */
narrow: number;
/** Normal column width */
normal: number;
/** Wide column width */
wide: number;
/** Wider column width */
wider: number;
}
import cssVariables from '../../actions/cssVariables/index';
interface Props {
/** Set to true for embeddables. */
embedded?: boolean;
/** Add an id to the article tag to target it with custom CSS. */
id?: string;
/** ARIA role of the article */
role?: string | null;
/** Set custom widths for the normal, wide and wider column dimensions */
columnWidths?: ColumnWidths;
children?: import('svelte').Snippet;
}
let {
embedded = false,
id = '',
role = null,
columnWidths = {
narrower: 330,
narrow: 510,
normal: 660,
wide: 930,
wider: 1200,
},
children,
}: Props = $props();
let columnWidthVars = $derived({
'narrower-column-width': columnWidths.narrower + 'px',
'narrow-column-width': columnWidths.narrow + 'px',
'normal-column-width': columnWidths.normal + 'px',
'wide-column-width': columnWidths.wide + 'px',
'wider-column-width': columnWidths.wider + 'px',
});
</script>
<main id="main-content">
<article {id} class:embedded {role} use:cssVariables={columnWidthVars}>
<!-- Article content -->
{@render children?.()}
</article>
</main>
<style lang="scss">
article {
width: 100%;
display: block;
margin: 0;
padding: 0 15px;
background-color: var(--theme-colour-background, transparent);
&.embedded {
overflow: auto;
}
}
</style>

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

@ -0,0 +1,68 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import BeforeAfter from './BeforeAfter.svelte';
const { Story } = defineMeta({
title: 'Components/Multimedia/BeforeAfter',
component: BeforeAfter,
argTypes: {
handleColour: { control: 'color' },
width: {
control: 'select',
options: ['normal', 'wide', 'wider', 'widest', 'fluid'],
},
},
});
</script>
<script>
import beforeImg from './images/myrne-before.jpg';
import afterImg from './images/myrne-after.jpg';
</script>
<Story
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 text" exportName="WithText">
<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."
>
{#snippet beforeOverlay()}
<div 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>
{/snippet}
{#snippet afterOverlay()}
<div 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>
{/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;
p {
color: #ffffff;
}
}
</style>
</Story>

View file

@ -0,0 +1,376 @@
<!-- @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 'es-toolkit';
import Block from '../Block/Block.svelte';
import PaddingReset from '../PaddingReset/PaddingReset.svelte';
import type { ContainerWidth } from '../@types/global';
import { random4 } from '../../utils/';
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;
}
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.
*/
let img: HTMLImageElement | undefined = $state(undefined);
/** Defaults with an empty DOMRect with all values set to 0 */
let imgOffset: DOMRect = $state(new DOMRect());
let sliding = false;
let figure: HTMLElement | undefined = $state(undefined);
let beforeOverlayWidth = $state(0);
let isFocused = false;
let containerWidth: number = $state(0); // Defaults to 0
let containerHeight = $derived(
containerWidth && heightRatio ? containerWidth * heightRatio : height
);
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
);
/** 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 { code, key } = e;
const margin = handleMargin / w;
if (code === 'ArrowRight' || key === 'ArrowRight') {
offset = Math.min(1 - margin, offset + keyPressStep);
} 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 =
e instanceof TouchEvent && e.touches ? e.touches[0] : (e as MouseEvent);
const figureOffset =
figure ?
parseInt(window.getComputedStyle(figure).marginLeft.slice(0, -2))
: 0;
let x = el.pageX - figureOffset - imgOffset.left;
x =
x < handleMargin ? handleMargin
: x > w - handleMargin ? w - handleMargin
: x;
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');
}
/** @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
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}>
<button
style={figStyle}
class="before-after-container relative overflow-hidden my-0 mx-auto"
ontouchstart={start}
onmousedown={start}
bind:this={figure}
aria-label={ariaLabel}
>
<img
bind:this={img}
src={afterSrc}
alt={afterAlt}
onload={measureLoadedImage}
style={imgStyle}
class="after absolute block m-0 max-w-full object-cover"
aria-describedby={beforeOverlay ?
`${id}-before-description`
: undefined}
/>
<img
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={afterOverlay ?
`${id}-after-description`
: undefined}
/>
{#if beforeOverlay}
<div
id="{id}-before-description"
class="overlay-container before absolute"
bind:clientWidth={beforeOverlayWidth}
style="clip-path: inset(0 {beforeOverlayClip}px 0 0);"
>
<!-- Overlay for before image -->
{@render beforeOverlay()}
</div>
{/if}
{#if afterOverlay}
<div
id="{id}-after-description"
class="overlay-container after absolute"
>
<!-- Overlay for after image -->
{@render afterOverlay()}
</div>
{/if}
<div
tabindex="0"
role="slider"
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};"
{onfocus}
{onblur}
>
<div class="arrow-left"></div>
<div class="arrow-right"></div>
</div>
</button>
</div>
{#if caption}
<PaddingReset containerIsFluid={width === 'fluid'}>
<aside class="before-after-caption mx-auto" id={`${id}-caption`}>
<!-- Caption for image credits -->
{@render caption()}
</aside>
</PaddingReset>
{/if}
</Block>
{/if}
<style lang="scss">
@use '../../scss/mixins' as mixins;
button.before-after-container {
box-sizing: content-box;
text-align: inherit;
img {
top: 0;
left: 0;
z-index: 20;
&.after {
z-index: 21;
}
&.before {
z-index: 22;
}
user-select: none;
}
.overlay-container {
top: 0;
:global(:first-child) {
margin-block-start: 0;
}
:global(:last-child) {
margin-block-end: 0;
}
&.before {
left: 0;
z-index: 23;
}
&.after {
right: 0;
z-index: 21;
}
}
}
.handle {
z-index: 30;
width: 40px;
height: 40px;
cursor: move;
background: none;
user-select: none;
position: absolute;
border-radius: 50px;
top: calc(50% - 20px);
border: 4px solid var(--before-after-handle-colour);
opacity: var(--before-after-handle-inactive-opacity, 0.6);
box-shadow: 1px 1px 3px #333;
&:hover,
&:active,
&:focus {
opacity: 1;
}
&:before,
&:after {
content: '';
box-shadow: 0 0 3px #333;
height: 9999px;
position: absolute;
left: calc(50% - 2px);
border: 2px solid var(--before-after-handle-colour);
}
&:before {
top: 40px;
}
&:after {
bottom: 40px;
}
.arrow-right,
.arrow-left {
width: 0;
height: 0;
user-select: none;
position: relative;
border-block-start: 10px solid transparent;
border-block-end: 10px solid transparent;
}
.arrow-right {
inset-inline-start: 19px;
inset-block-end: 14px;
border-inline-start: 10px solid var(--before-after-handle-colour);
}
.arrow-left {
inset-inline-start: 3px;
inset-block-start: 6px;
border-inline-end: 10px solid var(--before-after-handle-colour);
}
}
.before-after-caption {
:global(p) {
@include mixins.body-caption;
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 KiB

View file

@ -0,0 +1,99 @@
import { Meta, Canvas } from '@storybook/blocks';
import * as BlockStories from './Block.stories.svelte';
<Meta of={BlockStories} />
# Block
The `Block` component is the basic building block of pages, a responsive container that wraps around each section of your piece.
Blocks are stacked vertically within the well created by the [Article](./?path=/docs/components-page-layout-article--docs) component. They can have different widths on larger screens depending on the `width` prop.
> 📌 Many of our other components already use the `Block` component internally. You'll usually only need to use it yourself if you're making something custom.
```svelte
<script>
import { Block } from '@reuters-graphics/graphics-components';
</script>
<Block>
<!-- Contents for this block goes here -->
</Block>
```
<Canvas of={BlockStories.Demo} />
## Custom layouts
Our article well is designed to provide a basic responsive layout for you, but it also lets you customise.
The radical but easiest way to do this is to create a `Block` with a `fluid` width -- which basically cancels out the article well dimensions -- and then code whatever you need from scratch or with another framework.
The demo below does exactly that to create an edge-to-edge grid with [flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/).
```svelte
<Block width="fluid">
<div class="my-radical-container">
<!-- Now, you have full control over layout! -->
</div>
</Block>
```
<Canvas of={BlockStories.CustomLayout} />
## Snap widths
Normally, `Block` containers resize fluidly below the original `width`. Sometimes, though, you may want the container to snap to the next breakpoint -- for example, if you have a static graphic that looks fine at the set block breakpoints, but isn't so great at widths inbetween.
You can use the `snap` prop to force the container to snap to each block width successively as the window sizes down.
```svelte
<Block width="wider" snap={true}>
<!-- Contents for this block -->
</Block>
```
<Canvas of={BlockStories.SnapWidthsBasic} />
If you want to skip certain block widths entirely, you can add one or more class of `skip-{block width class}` to the `Block`.
> **NOTE:** The snap width breakpoints only work on `Block` components with widths `wider` and below. `widest` and `fluid` are both **always** fluid, since they go edge-to-edge.
```svelte
<!-- Will skip wide and go straight to normal column width on resize. -->
<Block width="wider" snap={true} class="skip-wide">
<!-- Contents for this block -->
</Block>
```
This is probably easier to see in action than explain in words, so [resize the demo](./?path=/docs/components-page-layout-block--snap-skip-widths) to get a better picture of how it all works.
## Using with custom column widths
Snap width breakpoints are hard-coded to the default article well column widths, so if you set custom `columnWidths` on the [Article](./?path=/docs/components-page-layout-article--docs) component (**rare!**), you need to do a littie work to use this functionality.
Luckily, it's still pretty easy. Just add a `cls` or `id` to your `Block` so you can target it with some custom SCSS. Then define a few SCSS variables corresponding to your custom column widths, and use the `block-snap-widths` SCSS mixin to get the same functionality at your custom breakpoints.
```svelte
<Block width="wider" snap={true} class="custom-blocks">
<!-- Contents for this block -->
</Block>
<style lang="scss">
// Define custom column widths
$column-width-narrower: 310px;
$column-width-narrow: 450px;
$column-width-normal: 600px;
$column-width-wide: 860px;
$column-width-wider: 1400px;
@use '@reuters-graphics/graphics-components/scss/mixins' as mixins;
:global {
div.custom-blocks {
@include mixins.block-snap-widths; // Use the `block-snap-widths` mixin
}
}
</style>
```

View file

@ -0,0 +1,181 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import Block from './Block.svelte';
const { Story } = defineMeta({
title: 'Components/Page layout/Block',
component: Block,
tags: ['autodocs'],
argTypes: {
width: {
control: 'select',
options: [
'narrower',
'narrow',
'normal',
'wide',
'wider',
'widest',
'fluid',
],
},
},
});
</script>
<script>
import Article from '../Article/Article.svelte';
</script>
<Story name="Demo">
<Article id="block-demo-article">
<div class="article-boundaries">
<div class="label">Article</div>
<Block>
<div class="label">Block</div>
</Block>
</div>
</Article>
</Story>
<Story name="Custom layout" exportName="CustomLayout">
<Block width="fluid">
<!-- Enter bootstrap grid! -->
<div id="block-flex-example">
<div class="row">
<div class="col">Column</div>
<div class="col-6">Column</div>
<div class="col">Column</div>
</div>
<div class="row">
<div class="col">Column</div>
<div class="col">Column</div>
</div>
</div>
</Block>
</Story>
<Story name="Snap widths" exportName="SnapWidthsBasic">
<Article id="block-demo-article">
<div class="article-boundaries">
<div class="label">Article</div>
<h4>snap widths</h4>
<Block snap={true}>
<div class="label">Block</div>
</Block>
</div>
</Article>
</Story>
<Story name="Snap and skip widths" exportName="SnapSkipWidths">
<Article id="block-demo-article">
<div class="article-boundaries">
<div class="label">Article</div>
<h4>Regular layout</h4>
<Block width="narrower" snap={true} class="block-snap-widths-demo">
narrower
</Block>
<Block width="narrow" snap={true} class="block-snap-widths-demo">
narrow
</Block>
<Block width="normal" snap={true} class="block-snap-widths-demo">
normal
</Block>
<Block width="wide" snap={true} class="block-snap-widths-demo">
wide
</Block>
<Block width="wider" snap={true} class="block-snap-widths-demo">
wider
</Block>
<h4>with snap and skip</h4>
<Block width="narrower" snap={true} class="block-snap-widths-demo even">
narrower
</Block>
<Block width="narrow" snap={true} class="block-snap-widths-demo even">
narrow
</Block>
<Block
width="normal"
snap={true}
class="block-snap-widths-demo even skip-narrow"
>
normal.skip-narrow
</Block>
<Block
width="wide"
snap={true}
class="block-snap-widths-demo even skip-normal skip-narrow"
>
wide.skip-normal.skip-narrow
</Block>
<Block
width="wider"
snap={true}
class="block-snap-widths-demo even skip-wide"
>
wider.skip-wide
</Block>
</div>
</Article>
</Story>
<style lang="scss">
h4 {
text-align: center;
}
:global(#block-demo-article) {
background-color: #ddd;
position: relative;
width: calc(100% + 30px);
margin-inline-start: -15px;
}
:global(#block-demo-article .article-boundaries) {
padding: 0 0 18px;
width: 100%;
height: 100%;
background-color: #bbb;
}
:global(#block-demo-article div.article-block) {
height: 100px;
background: #81a1c1;
}
:global(#block-demo-article div.article-block.block-snap-widths-demo) {
margin-block-end: 2px;
height: 40px;
font-size: 11px;
}
:global(#block-demo-article div.article-block.block-snap-widths-demo.even) {
background: rgb(211, 132, 123);
}
:global(
#block-demo-article .label,
#block-demo-article div.article-block.block-snap-widths-demo
) {
padding-inline-start: 3px;
color: white;
font-weight: 500;
}
div#block-flex-example {
padding: 25px 0;
div.row {
display: flex;
}
div.row > div {
background-color: rgb(211, 132, 123);
border: 1px solid white;
border-radius: 4px;
padding: 20px;
color: white;
text-align: center;
flex-grow: 1;
}
div.row:first-child {
div {
background: #81a1c1;
}
}
}
</style>

View file

@ -0,0 +1,82 @@
<!-- @component `Block` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-page-layout-block--docs) -->
<script lang="ts">
import type { Snippet } from 'svelte';
import type { ContainerWidth } from '../@types/global';
interface Props {
/** Content that goes inside `<Block>`*/
children: Snippet;
/** Width of the block within the article well. */
width?: ContainerWidth;
/** Add an id to the block tag to target it with custom CSS. */
id?: string;
/** Add custom classes to the block tag to target it with custom CSS. */
class?: string;
/** Snap block to column widths, rather than fluidly resizing them. */
snap?: boolean;
/** ARIA [role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles) for the block */
role?: string;
/** ARIA [label](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-label) for the block */
ariaLabel?: string;
}
let {
children,
width = 'normal',
id = '',
class: cls = '',
snap = false,
role,
ariaLabel,
}: Props = $props();
</script>
<div
{id}
class="article-block fmx-auto {width} {cls}"
class:snap={snap && width !== 'fluid' && width !== 'widest'}
{role}
aria-label={ariaLabel}
>
{@render children()}
</div>
<style lang="scss">
@use '../../scss/mixins' as mixins;
.article-block {
max-width: var(--normal-column-width, 660px);
&.narrower {
max-width: var(--narrower-column-width, 330px);
}
&.narrow {
max-width: var(--narrow-column-width, 510px);
}
&.wide {
max-width: var(--wide-column-width, 930px);
}
&.wider {
max-width: var(--wider-column-width, 1200px);
}
&.widest {
max-width: 100%;
}
&.fluid {
width: calc(100% + 30px);
margin-inline-start: -15px;
max-width: none;
}
// Only setup for the default column widths, b/c we can't use
// CSS vars in media queries.
&.snap {
@include mixins.block-snap-widths;
}
}
</style>

View file

@ -0,0 +1,115 @@
import { Meta, Canvas } from '@storybook/blocks';
import * as BodyTextStories from './BodyText.stories.svelte';
<Meta of={BodyTextStories} />
# BodyText
The `BodyText` component creates the main text of your page. You can pass the `text` prop a [markdown-formatted](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) string, which will be parsed into paragraphs, headers, lists, links, blockquotes and other markdown-supported elements.
```svelte
<script>
import { BodyText } from '@reuters-graphics/graphics-components';
const markdownText = `Bacon ipsum **dolor amet** cow tongue tri-tip.
Biltong turducken ground round kevin [hamburger turkey](https://reuters.com) pig.
- Steak
- [Pork chop](https://www.google.com)
- Fillet
Venison shoulder *ham hock ham leberkas*. Flank beef ribs fatback, jerky meatball ham hock.`;
</script>
<BodyText text={markdownText} />
```
<Canvas of={BodyTextStories.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: text
text: Bacon ipsum ...
... etc.
:end
[]
```
... which you'll parse out of a ArchieML block object before passing to the `BodyText` component.
```svelte
<!-- App.svelte -->
<script>
import { BodyText } from '@reuters-graphics/graphics-components';
import content from '$locales/en/content.json';
</script>
{#each content.blocks as block}
{#if block.type === 'text'}
<BodyText text={block.text} />
{/if}
{/each}
```
<Canvas of={BodyTextStories.Demo} />
## Styling text
Styles are built in for many text elements created by `BodyText`, including headings, ordered and unordered lists, links, blockquotes and even drop caps (using a `"drop-cap"` classed span).
```svelte
<BodyText
text="<span class='drop-cap'>R</span>eprehenderit hamburger pork bresaola ..."
/>
```
<Canvas of={BodyTextStories.TypographySample} />
### Custom styles
To add your own styling, you can write styles in a global SCSS stylesheet:
```svelte
<BodyText
text="Venison shoulder <span class='highlight'>ham hock</span> ham leberkas."
/>
```
```scss
// global.scss
span.highlight {
background: palegoldenrod;
padding: 2px 4px;
}
```
<Canvas of={BodyTextStories.CustomStyles} />
If you want to make sure styles for one portion of text don't apply other parts of the page, add a `class` to BodyText to use as an additional selector.
```svelte highlight=2
<BodyText
class="my-special-text-block"
text="Venison shoulder <span class='highlight'>ham hock</span> ham leberkas."
/>
```
```scss
// global.scss
.my-special-text-block {
span.highlight {
background: palegoldenrod;
padding: 2px 4px;
}
}
```

View file

@ -0,0 +1,102 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import BodyText from './BodyText.svelte';
const { Story } = defineMeta({
title: 'Components/Text elements/BodyText',
component: BodyText,
});
</script>
<Story
name="Demo"
args={{
text: `Bacon ipsum **dolor amet** cow tongue tri-tip.
Biltong turducken ground round kevin [hamburger turkey](https://reuters.com) pig.
- Steak
- [Pork chop](https://www.google.com)
- Fillet
Venison shoulder *ham hock ham leberkas*. Flank beef ribs fatback, jerky meatball ham hock.`,
}}
/>
<Story
name="Typography sample"
exportName="TypographySample"
tags={['!autodocs', '!dev']}
args={{
class: 'body-text-typography-example-story',
text: `<span class='drop-cap'>R</span>eprehenderit hamburger pork bresaola, dolore chuck sirloin landjaeger ham hock [tempor meatball](https://baconipsum.com/) alcatra nostrud pork belly. Culpa pork belly doner ea jowl, elit deserunt leberkas cow shoulder ham hock dolore.
## Biltong turducken ground round kevin
Pig est irure buffalo ullamco. Sunt beef ribs tri-tip, chislic officia sint dolor. Spare ribs drumstick ground round, irure duis cillum id chicken est ipsum ut.
Qui cupidatat chislic buffalo consequat deserunt.
Andouille sint shankle quis velit nostrud chislic meatloaf culpa labore corned beef chuck spare ribs. Filet mignon eu shankle in, meatloaf ut dolor ham hock ut.
### Venison shoulder ham hock ham leberkas flank beef ribs fatback, jerky meatball ham hock
Biltong turducken ground round kevin [hamburger turkey](https://reuters.com) pig. Veniam laboris sunt chislic. Aute doner porchetta nulla, tongue venison ad ex in do.
- Steak
- Capicola
- [Pork chop](https://www.google.com)
- Fillet landjaeger commodo
Venison shoulder *ham hock ham leberkas*. Flank beef ribs fatback, jerky meatball ham hock.
Minim id buffalo dolore ad, **boudin chicken laboris** excepteur qui eiusmod.
#### Jerky prosciutto burgdoggen
Sirloin beef flank labore cillum venison pariatur cow nulla ut irure in consequat proident velit. Jerky meatball pig nulla irure laboris fatback et rump ut dolore.
Biltong enim consequat pork chop, flank ea.
> Officia ball tip sed tenderloin dolore. Est magna enim, turkey in turducken flank jowl ad lorem buffalo ground
> > Ronald McDonald
Flank bacon sint dolore porchetta strip steak. Tail capicola flank nostrud meatball consequat pastrami lorem cupidatat chuck drumstick ham hock bresaola sint.
##### Venison pork chop
Alcatra bacon mollit boudin. Capicola ut tongue biltong, cow cillum pariatur sausage.
1. Minim ribeye
2. Prosciutto laborum
3. Salami doner irure
Consectetur ribeye consequat pork capicola. T-bone ad laborum beef ribs picanha.
###### Alcatra bacon mollit boudin
Tempor tail doner chicken incididunt beef ribs. Ad ullamco in cupim venison. Leberkas rump ullamco adipisicing, laboris excepteur voluptate.
Ham hock id porchetta elit. Sint spare ribs aute buffalo.
<p class='body-correction'>Correction: Lorem ispsum dolor sit amet ameno dorime.</p>
`,
}}
/>
<Story
name="Custom styles"
exportName="CustomStyles"
tags={['!autodocs', '!dev']}
args={{
class: 'body-text-custom-styles-story',
text: `Venison shoulder <span class="highlight">ham hock</span> ham leberkas.`,
}}
/>
<style lang="scss">
:global(.body-text-custom-styles-story span.highlight) {
background: palegoldenrod;
padding: 2px 4px;
}
</style>

View file

@ -0,0 +1,19 @@
<!-- @component `BodyText` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-text-elements-bodytext--docs) -->
<script lang="ts">
import { Markdown } from '@reuters-graphics/svelte-markdown';
import Block from '../Block/Block.svelte';
interface Props {
/** A markdown text string. */
text: string;
/** Add a class to target with SCSS. */
class?: string;
/** Add an id to the block tag to target it with custom CSS. */
id?: string;
}
let { text, class: cls = '', id = '' }: Props = $props();
</script>
<Block {id} class="fmy-6 {cls}">
<Markdown source={text} />
</Block>

View file

@ -0,0 +1,110 @@
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} />
## Custom byline, published and updated datelines
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} />
## Custom author page
By default, the `Byline` component will hyperlink each author's byline to their Reuters.com page, formatted `https://www.reuters.com/authors/{author-slug}/`.
To hyperlink to different pages or email addresses, pass a custom function to the `getAuthorPage` prop.
```svelte
<!-- Pass a custom function as `getAuthorPage` -->
<Byline
authors={[
'Dea Bankova',
'Prasanta Kumar Dutta',
'Anurag Rao',
'Mariano Zafra',
]}
publishTime="2021-09-12T00:00:00Z"
updateTime="2021-09-12T13:57:00Z"
getAuthorPage={(author) => {
return `mailto:${author.replace(' ', '')}@example.com`;
}}
/>
```
<Canvas of={BylineStories.CustomAuthorPage} />
````

View file

@ -0,0 +1,63 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import Byline from './Byline.svelte';
const { Story } = defineMeta({
title: 'Components/Text elements/Byline',
component: Byline,
tags: ['autodocs'],
argTypes: {
align: {
control: 'select',
options: ['auto', 'center'],
},
},
});
</script>
<Story
name="Demo"
args={{
authors: [
'Dea Bankova',
'Prasanta Kumar Dutta',
'Anurag Rao',
'Mariano Zafra',
],
publishTime: new Date('2021-09-12').toISOString(),
updateTime: new Date('2021-09-12T13:57:00').toISOString(),
}}
/>
<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>
<Story
name="Custom author page"
exportName="CustomAuthorPage"
tags={['!autodocs', '!dev']}
args={{
authors: [
'Dea Bankova',
'Prasanta Kumar Dutta',
'Anurag Rao',
'Mariano Zafra',
],
publishTime: '2021-09-12T00:00:00Z',
updateTime: '2021-09-12T13:57:00Z',
getAuthorPage: (author: string) => {
return `mailto:${author.replace(' ', '')}@example.com`;
},
}}
/>

View file

@ -0,0 +1,188 @@
<!-- @component `Byline` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-text-elements-byline--docs) -->
<script lang="ts">
import { getAuthorPageUrl } from '../../utils';
import Block from '../Block/Block.svelte';
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?: 'auto' | '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.
*/
published?: Snippet;
/**
* Optional snippet for a custom updated dateline.
*/
updated?: Snippet;
}
let {
authors = [],
publishTime,
updateTime,
align = 'auto',
id = '',
cls = '',
getAuthorPage = getAuthorPageUrl,
byline,
published,
updated,
}: Props = $props();
let alignmentClass = $derived(align === 'center' ? 'text-center' : '');
/**
/* Date validation and formatter functions
*/
const isValidDate = (datetime: string) => {
if (!datetime) return false;
if (!Date.parse(datetime)) return false;
return true;
};
const formatTime = (datetime: string) =>
new Date(datetime).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
timeZoneName: 'short',
});
const areSameDay = (first: Date, second: Date) =>
first.getFullYear() === second.getFullYear() &&
first.getMonth() === second.getMonth() &&
first.getDate() === second.getDate();
</script>
<Block {id} class="byline-container {alignmentClass} {cls}" width="normal">
<aside class="article-metadata font-subhed">
<div class="byline body-caption fmb-1">
{#if byline}
<!-- Custom 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)}
rel="author"
>
{author.trim()}</a
>{#if authors.length > 1 && i < authors.length - 2},{/if}
{#if authors.length > 1 && i === authors.length - 2}and&nbsp;{/if}
{/each}
{:else}
<a
href="https://www.reuters.com"
class="no-underline whitespace-nowrap text-primary font-bold"
>Reuters</a
>
{/if}
{/if}
</div>
<div class="dateline body-caption fmt-0">
{#if published}
<div class="whitespace-nowrap inline-block">
<!-- 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}>
{#if updateTime && isValidDate(updateTime)}
{apdate(new Date(publishTime))}
{:else}
{apdate(new Date(publishTime))}&nbsp;&nbsp;{formatTime(
publishTime
)}
{/if}
</time>
</div>
{/if}
{#if updated}
<div class="whitespace-nowrap inline-block">
<!-- 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}>
{#if areSameDay(new Date(publishTime), new Date(updateTime || new Date()))}
{formatTime(updateTime || '')}
{:else}
{apdate(
new Date(updateTime || new Date())
)}&nbsp;&nbsp;{formatTime(updateTime || '')}
{/if}
</time>
</div>
{/if}
</div>
</aside>
</Block>
<style lang="scss">
@use '../../scss/mixins' as *;
.byline {
a {
&:hover {
text-decoration-line: underline;
}
}
}
@media (min-width: $column-width-narrower) {
.dateline {
div {
&:not(:last-child) {
&:after {
content: '·';
margin: 0 2px 0 5px;
}
}
}
}
}
</style>

View file

@ -0,0 +1,45 @@
import { Meta, Canvas } from '@storybook/blocks';
import * as DatawrapperChartStories from './DatawrapperChart.stories.svelte';
<Meta of={DatawrapperChartStories} />
# DatawrapperChart
Easily add a responsive Datawrapper embed on your page.
```svelte
<script>
import { DatawrapperChart } from '@reuters-graphics/graphics-components';
</script>
<DatawrapperChart
title="Global abortion access"
ariaLabel="map"
id="abortion-rights-map"
src="https://graphics.reuters.com/USA-ABORTION/lgpdwggnwvo/media-embed.html"
/>
```
##### Getting the chart URL for `src`
Copy the source url for the Datawrapper chart in the `src` prop.
You can get this from the published url on Reuters Graphics.
- Publish the chart on Datawrapper.
- Go to the **Datawrapper charts** Teams channel, wait for the graphic to finish publishing.
- Inside **Embed code (for developers only)**, find and copy the url inside the `src` prop. (It ends in `media-embed.html`.)
**Note:** There is no need to update the url if you update the chart inside Datawrapper. Any changes will be automatically reflected.
<Canvas of={DatawrapperChartStories.Demo} />
## With chatter
By default, Datawrapper will export your chart with the chart chatter like title, description and notes.
At the moment, these don't _exactly_ match our styles and can't be made to fit into the article well.
Instead, it's often better to remove all the text from your Datawrapper chart before publishing it and add that text back via the component props.
<Canvas of={DatawrapperChartStories.WithChatter} />

View file

@ -0,0 +1,41 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import DatawrapperChart from './DatawrapperChart.svelte';
const { Story } = defineMeta({
title: 'Components/Graphics/DatawrapperChart',
component: DatawrapperChart,
tags: ['autodocs'],
argTypes: {
width: {
control: 'select',
options: ['normal', 'wide', 'wider', 'widest', 'fluid'],
},
},
});
</script>
<Story
name="Demo"
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={{
frameTitle: 'Global abortion access',
ariaLabel: 'map',
id: 'abortion-rights-map',
src: 'https://reuters.com/graphics/USA-ABORTION/lgvdwemlbpo/media-embed.html',
title: 'Global abortion access',
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,117 @@
<!-- @component `DatawrapperChart` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-graphics-datawrapperchart--docs) -->
<script lang="ts">
import { onMount, onDestroy, type Snippet } from 'svelte';
import GraphicBlock from '../GraphicBlock/GraphicBlock.svelte';
import type { ContainerWidth } from '../@types/global';
type ScrollingOption = 'auto' | 'yes' | 'no';
interface Props {
/** Title of the graphic */
title?: string;
/** Description of the graphic, passed in as a markdown string. */
description?: string;
/**
* iframe title
*/
frameTitle: string;
/**
* Notes to the graphic, passed in as a markdown string.
*/
notes?: string;
/**
* iframe aria label
*/
ariaLabel: string;
/*
* iframe id
*/
id: string;
/**
* Datawrapper embed URL
*/
src: string;
/** iframe scrolling option */
scrolling: ScrollingOption;
/** Width of the chart within the text well. */
width: ContainerWidth;
/**
* Set a different width for the text within the text well, for example,
* "normal" to keep the title, description and notes inline with the rest
* of the text well. Can't ever be wider than `width`.
*/
textWidth: ContainerWidth;
/** Custom headline and chatter snippet */
titleSnippet?: Snippet;
/** Custom notes and source snippet */
notesSnippet?: Snippet;
}
let {
title,
description,
frameTitle = '',
notes,
ariaLabel = '',
id = '',
src,
scrolling = 'no',
width = 'normal',
textWidth = 'normal',
titleSnippet,
notesSnippet,
}: Props = $props();
let frameElement: HTMLElement;
// eslint-disable-next-line
const frameFiller = (e: any) => {
if (void 0 !== e.data['datawrapper-height']) {
const t = [frameElement];
for (const a in e.data['datawrapper-height']) {
for (let r = 0; r < t.length; r++) {
// @ts-ignore OK here
if (t[r].contentWindow === e.source) {
t[r].style.height = e.data['datawrapper-height'][a] + 'px';
}
}
}
}
};
onMount(() => {
if (typeof window !== 'undefined') {
window.addEventListener('message', frameFiller);
}
});
onDestroy(() => {
if (typeof window !== 'undefined') {
window.removeEventListener('message', frameFiller);
}
});
</script>
<GraphicBlock {width} {textWidth} {title} {description} {notes}>
{#if titleSnippet}
<!-- Custom headline and chatter slot -->
{@render titleSnippet()}
{/if}
<div class="datawrapper-chart">
<iframe
bind:this={frameElement}
title={frameTitle}
aria-label={ariaLabel}
{id}
{src}
{scrolling}
frameborder="0"
data-chromatic="ignore"
style="width: 0; min-width: 100% !important; border: none;"
></iframe>
</div>
{#if notesSnippet}
{@render notesSnippet()}
{/if}
</GraphicBlock>

View file

@ -0,0 +1,26 @@
import { Meta, Canvas } from '@storybook/blocks';
import * as DocumentCloudStories from './DocumentCloud.stories.svelte';
<Meta of={DocumentCloudStories} />
# DocumentCloud
The `DocumentCloud` component embeds a document hosted by [DocumentCloud](https://documentcloud.org).
The document must have its access level set to **public** before it can be embedded. The `slug` can be found after the final slash in the document's URL.
For instance, the document included in the example is found at [documentcloud.org/documents/3259984-Trump-Intelligence-Allegations](https://www.documentcloud.org/documents/3259984-Trump-Intelligence-Allegations). The `slug` is `3259984-Trump-Intelligence-Allegations`.
```svelte
<script>
import { DocumentCloud } from '@reuters-graphics/graphics-components';
</script>
<DocumentCloud
slug="3259984-Trump-Intelligence-Allegations"
altText="These Reports Allege Trump Has Deep Ties To Russia"
/>
```
<Canvas of={DocumentCloudStories.Demo} />

View file

@ -0,0 +1,23 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import DocumentCloud from './DocumentCloud.svelte';
const { Story } = defineMeta({
title: 'Components/Multimedia/DocumentCloud',
component: DocumentCloud,
argTypes: {
width: {
control: 'select',
options: ['normal', 'wide', 'wider', 'widest', 'fluid'],
},
},
});
</script>
<Story
name="Demo"
args={{
slug: '3259984-Trump-Intelligence-Allegations',
altText: 'These Reports Allege Trump Has Deep Ties To Russia',
}}
/>

View file

@ -0,0 +1,41 @@
<!-- @component `DocumentCloud` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-multimedia-documentcloud--docs) -->
<script lang="ts">
import type { ContainerWidth } from '../@types/global';
import Block from '../Block/Block.svelte';
interface Props {
/**
* The unique identifier for the document.
*/
slug: string;
/**
* Alt text for the document.
*/
altText: string;
/**
* Width of the container, one of: normal, wide, wider, widest or fluid
*/
width?: ContainerWidth;
/** Add an ID to target with SCSS. */
id?: string;
/** Add a class to target with SCSS. */
class?: string; // Add a class to target with SCSS.
}
let {
slug,
altText,
width = 'normal',
id = '',
class: cls = '',
}: Props = $props();
</script>
<Block {width} {id} class="photo fmy-6 {cls}">
<iframe
class="h-screen"
src="https://embed.documentcloud.org/documents/{slug}/?embed=1&amp;responsive=1&amp;title=1"
title={altText}
sandbox="allow-scripts allow-same-origin allow-popups allow-forms allow-popups-to-escape-sandbox"
></iframe>
</Block>

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

@ -0,0 +1,17 @@
<script module lang="ts">
import EmbedPreviewerLink from './EmbedPreviewerLink.svelte';
import { defineMeta } from '@storybook/addon-svelte-csf';
const { Story } = defineMeta({
title: 'Components/Utilities/EmbedPreviewerLink',
component: EmbedPreviewerLink,
});
</script>
<Story
name="Demo"
tags={['!autodocs', '!dev']}
args={{
dev: true,
}}
/>

View file

@ -0,0 +1,32 @@
<script lang="ts">
import Fa from 'svelte-fa';
import { faWindowRestore } from '@fortawesome/free-regular-svg-icons';
interface Props {
dev?: boolean;
}
let { dev = false }: Props = $props();
</script>
{#if dev}
<div>
<a rel="external" href="/embed-previewer">
<Fa icon={faWindowRestore} />
</a>
</div>
{/if}
<style lang="scss">
div {
position: fixed;
bottom: 5px;
left: 10px;
font-size: 18px;
a {
color: #ccc;
&:hover {
color: #666;
}
}
}
</style>

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

@ -0,0 +1,29 @@
<script module lang="ts">
import EndNotes from './EndNotes.svelte';
import { defineMeta } from '@storybook/addon-svelte-csf';
const { Story } = defineMeta({
title: 'Components/Text elements/EndNotes',
component: EndNotes,
});
</script>
<script>
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>
<Story name="Demo" args={{ notes }} />

View file

@ -0,0 +1,58 @@
<!-- @component `EndNotes` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-text-elements-endnotes--docs) -->
<script lang="ts">
interface EndNote {
/**
* Title of the note item
*/
title: string;
/**
* Contents of the note as a markdown string
*/
text: string;
}
import Block from '../Block/Block.svelte';
import { Markdown } from '@reuters-graphics/svelte-markdown';
interface Props {
/**
* An array of endnote items.
*/
notes: EndNote[];
}
let { notes }: Props = $props();
</script>
<Block class="notes fmt-6 fmb-8">
{#each notes as note}
<div class="note-title">
<Markdown source={note.title} />
</div>
<div class="note-content">
<Markdown source={note.text} />
</div>
{/each}
</Block>
<style lang="scss">
@use '../../scss/mixins' as mixins;
.note-title {
:global(p) {
@include mixins.body-caption;
@include mixins.text-primary;
@include mixins.font-medium;
@include mixins.tracking-normal;
@include mixins.fmt-3;
margin-block-end: 0.125rem;
text-transform: none;
}
}
.note-content {
:global(p) {
@include mixins.body-caption;
@include mixins.fmt-0;
}
}
</style>

View file

@ -0,0 +1,72 @@
import { Meta, Canvas } from '@storybook/blocks';
import * as FeaturePhotoStories from './FeaturePhoto.stories.svelte';
<Meta of={FeaturePhotoStories} />
# FeaturePhoto
The `FeaturePhoto` component adds a full-width photo.
```svelte
<script>
import { FeaturePhoto } from '@reuters-graphics/graphics-components';
import { assets } from '$app/paths'; // 👈 If using in the graphics kit...
</script>
<FeaturePhoto
src={`${assets}/images/myImage.jpg`}
altText="Some alt text"
caption="A caption"
/>
```
<Canvas of={FeaturePhotoStories.Demo} />
## Using with ArchieML docs
With the graphics kit, you'll likely get your text value from an ArchieML doc...
```yaml
# ArchieML doc
[blocks]
type: photo
width: normal
src: images/shark.jpg
altText: The king of the sea
caption: Carcharodon carcharias - REUTERS
[]
```
... which you'll parse out of a ArchieML block object before passing to the `FeaturePhoto` component.
```svelte
<!-- App.svelte -->
<script>
import { FeaturePhoto } from '@reuters-graphics/graphics-components';
import content from '$locales/en/content.json';
import { assets } from '$app/paths';
</script>
{#each content.blocks as block}
{#if block.Type === 'text'}
<!-- ... -->
{:else if block.type === 'photo'}
<FeaturePhoto
width={block.width}
src={`${assets}/${block.src}`}
altText={block.altText}
caption={block.caption}
/>
{/if}
{/each}
```
## Missing alt text
`altText` is required in this component. If your photo is missing it, a small red text box will overlay the image.
<Canvas of={FeaturePhotoStories.MissingAltText} />

View file

@ -0,0 +1,41 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import FeaturePhoto from './FeaturePhoto.svelte';
const { Story } = defineMeta({
title: 'Components/Multimedia/FeaturePhoto',
component: FeaturePhoto,
argTypes: {
width: {
control: 'select',
options: ['normal', 'wide', 'wider', 'widest', 'fluid'],
},
textWidth: {
control: 'select',
options: ['normal', 'wide', 'wider', 'widest', 'fluid'],
},
},
});
</script>
<script>
import sharkSrc from './images/shark.jpg';
</script>
<Story
name="Demo"
args={{
src: sharkSrc,
altText: 'A shark!',
caption: 'Carcharodon carcharias - REUTERS',
}}
/>
<Story
name="Missing altText"
exportName="MissingAltText"
args={{
src: sharkSrc,
caption: 'Carcharodon carcharias - REUTERS',
}}
/>

View file

@ -0,0 +1,145 @@
<!-- @component `FeaturePhoto` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-multimedia-featurephoto--docs) -->
<script lang="ts">
import { onMount } from 'svelte';
import type { ContainerWidth } from '../@types/global';
import Block from '../Block/Block.svelte';
import PaddingReset from '../PaddingReset/PaddingReset.svelte';
interface Props {
/**
* Photo source
*/
src: string;
/**
* Photo altText
*/
altText: string;
/**
* Add an id to target with custom CSS.
*/
id?: string;
/**
* Add classes to target with custom CSS.
*/
class?: string;
/**
* Photo caption
*/
caption?: string;
/**
* Height of the photo placeholder when lazy-loading
*/
height?: number;
/**
* Width of the container: normal, wide, wider, widest or fluid
*/
width?: ContainerWidth;
/**
* Set a different width for the text vs the photo. For example, "normal" to keep the title, description and notes inline with the rest of the text well. Can't ever be wider than `width`.
*/
textWidth?: ContainerWidth;
/**
* Whether to lazy load the photo using the [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)
*/
lazy?: boolean;
/**
* Set Intersection Observer [rootMargin](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#rootmargin) `top` when lazy loading.
*/
top?: number;
/**
* Set Intersection Observer [rootMargin](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#rootmargin) `bottom` when lazy loading.
*/
bottom?: number;
/**
* Set Intersection Observer [rootMargin](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#rootmargin) `left` when lazy loading.
*/
left?: number;
/**
* Set Intersection Observer [rootMargin](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#rootmargin) `right` when lazy loading.
*/
right?: number;
}
let {
src,
altText,
id = '',
class: cls = '',
caption,
height = 100,
width = 'normal',
textWidth = 'normal',
lazy = true,
top = 0,
bottom = 0,
left = 0,
right = 0,
}: Props = $props();
let intersecting = $state(false);
let container: HTMLElement;
const intersectable = typeof IntersectionObserver !== 'undefined';
onMount(() => {
if (!lazy) return;
if (intersectable) {
const rootMargin = `${bottom}px ${left}px ${top}px ${right}px`;
const observer = new IntersectionObserver(
(entries) => {
intersecting = entries[0].isIntersecting;
if (intersecting) {
observer.unobserve(container);
}
},
{
rootMargin,
}
);
observer.observe(container);
return () => observer.unobserve(container);
}
});
</script>
<Block {width} class="photo fmy-6 {cls}" {id}>
<figure
bind:this={container}
aria-label="media"
class="w-full flex flex-col relative"
>
{#if !lazy || (intersectable && intersecting)}
<img class="w-full my-0" {src} alt={altText} />
{:else}
<div class="placeholder w-full" style={`height: ${height}px;`}></div>
{/if}
{#if caption}
<PaddingReset containerIsFluid={width === 'fluid'}>
<Block width={textWidth} class="notes w-full fmy-0">
<figcaption>
{caption}
</figcaption>
</Block>
</PaddingReset>
{/if}
{#if !altText}
<div class="alt-warning absolute text-xxs py-1 px-2">altText</div>
{/if}
</figure>
</Block>
<style lang="scss">
.placeholder {
background-color: #ccc;
}
div.alt-warning {
background-color: red;
color: white;
top: 0;
right: 0;
}
</style>

Some files were not shown because too many files have changed in this diff Show more