Squashed 'graphics-components-src/' content from commit 247be9ce
git-subtree-dir: graphics-components-src git-subtree-split: 247be9ce40bd338d3934534fb6392504a0cdc81f
This commit is contained in:
commit
d04c218f36
593 changed files with 54517 additions and 0 deletions
8
.changeset/README.md
Normal file
8
.changeset/README.md
Normal 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
11
.changeset/config.json
Normal 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
1
.github/CODEOWNERS
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
* @hobbes7878
|
||||
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal 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
29
.github/workflows/check.yaml
vendored
Normal 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
31
.github/workflows/chromatic.yaml
vendored
Normal 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
11
.github/workflows/docs.yaml
vendored
Normal 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
11
.github/workflows/lint.yaml
vendored
Normal 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
32
.github/workflows/pkg.pr.new.yaml
vendored
Normal 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
29
.github/workflows/release.yaml
vendored
Normal 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
194
.gitignore
vendored
Normal 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
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
engine-strict=true
|
||||
5
.prettierignore
Normal file
5
.prettierignore
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
docs
|
||||
dist
|
||||
node_modules
|
||||
!src/docs/
|
||||
pnpm-lock.yaml
|
||||
10
.prettierrc.js
Normal file
10
.prettierrc.js
Normal 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
9
.storybook/Theme.ts
Normal 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
12
.storybook/Wrapper.svelte
Normal 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
18
.storybook/main.ts
Normal 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
25
.storybook/manager.ts
Normal 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,
|
||||
});
|
||||
13
.storybook/preview-head.html
Normal file
13
.storybook/preview-head.html
Normal 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
136
.storybook/preview.scss
Normal 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
68
.storybook/preview.ts
Normal 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
7
.storybook/svelte-highlighting.d.ts
vendored
Normal 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;
|
||||
149
.storybook/svelte-highlighting.js
Normal file
149
.storybook/svelte-highlighting.js
Normal 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(/&/, '&');
|
||||
}
|
||||
});
|
||||
|
||||
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
145
.storybook/syntax.scss
Normal 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
7
.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"unifiedjs.vscode-mdx",
|
||||
"somewhatstationery.some-sass",
|
||||
"svelte.svelte-vscode"
|
||||
]
|
||||
}
|
||||
18
.vscode/settings.json
vendored
Normal file
18
.vscode/settings.json
vendored
Normal 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
8
.vscode/svelte.styles.code-snippets
vendored
Normal 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
278
CHANGELOG.md
Normal 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
66
CONTRIBUTING.md
Normal 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
9
README.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||

|
||||
|
||||
# ⚙️ graphics-components
|
||||
|
||||
[](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
6
chromatic.config.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"onlyChanged": true,
|
||||
"projectId": "Project:64a5c42823795823edcb60f4",
|
||||
"zip": true,
|
||||
"buildScriptName": "build:docs"
|
||||
}
|
||||
50
eslint.config.js
Normal file
50
eslint.config.js
Normal 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
14
knip.config.ts
Normal 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
143
package.json
Normal 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
9313
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
67
public/logo.svg
Normal file
67
public/logo.svg
Normal 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 |
39
src/actions/cssVariables/cssVariables.mdx
Normal file
39
src/actions/cssVariables/cssVariables.mdx
Normal 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>
|
||||
```
|
||||
20
src/actions/cssVariables/index.ts
Normal file
20
src/actions/cssVariables/index.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
};
|
||||
25
src/actions/resizeObserver/index.ts
Normal file
25
src/actions/resizeObserver/index.ts
Normal 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);
|
||||
},
|
||||
};
|
||||
};
|
||||
20
src/actions/resizeObserver/resizeObserver.mdx
Normal file
20
src/actions/resizeObserver/resizeObserver.mdx
Normal 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
11
src/app.html
Normal 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>
|
||||
128
src/components/@types/global.ts
Normal file
128
src/components/@types/global.ts
Normal 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;
|
||||
}
|
||||
52
src/components/AdSlot/@types/ads.ts
Normal file
52
src/components/AdSlot/@types/ads.ts
Normal 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'];
|
||||
35
src/components/AdSlot/AdScripts.svelte
Normal file
35
src/components/AdSlot/AdScripts.svelte
Normal 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 />
|
||||
64
src/components/AdSlot/AdSlot.svelte
Normal file
64
src/components/AdSlot/AdSlot.svelte
Normal 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>
|
||||
56
src/components/AdSlot/InlineAd.mdx
Normal file
56
src/components/AdSlot/InlineAd.mdx
Normal 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} />
|
||||
20
src/components/AdSlot/InlineAd.stories.svelte
Normal file
20
src/components/AdSlot/InlineAd.stories.svelte
Normal 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} />
|
||||
74
src/components/AdSlot/InlineAd.svelte
Normal file
74
src/components/AdSlot/InlineAd.svelte
Normal 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>
|
||||
31
src/components/AdSlot/LeaderboardAd.mdx
Normal file
31
src/components/AdSlot/LeaderboardAd.mdx
Normal 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} />
|
||||
29
src/components/AdSlot/LeaderboardAd.stories.svelte
Normal file
29
src/components/AdSlot/LeaderboardAd.stories.svelte
Normal 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>
|
||||
102
src/components/AdSlot/LeaderboardAd.svelte
Normal file
102
src/components/AdSlot/LeaderboardAd.svelte
Normal 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>
|
||||
44
src/components/AdSlot/OneTrust.svelte
Normal file
44
src/components/AdSlot/OneTrust.svelte
Normal 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>
|
||||
78
src/components/AdSlot/ResponsiveAd.svelte
Normal file
78
src/components/AdSlot/ResponsiveAd.svelte
Normal 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}
|
||||
37
src/components/AdSlot/SponsorshipAd.mdx
Normal file
37
src/components/AdSlot/SponsorshipAd.mdx
Normal 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} />
|
||||
20
src/components/AdSlot/SponsorshipAd.stories.svelte
Normal file
20
src/components/AdSlot/SponsorshipAd.stories.svelte
Normal 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} />
|
||||
85
src/components/AdSlot/SponsorshipAd.svelte
Normal file
85
src/components/AdSlot/SponsorshipAd.svelte
Normal 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>
|
||||
105
src/components/AdSlot/adScripts/bootstrap.ts
Normal file
105
src/components/AdSlot/adScripts/bootstrap.ts
Normal 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
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
12
src/components/AdSlot/adScripts/getParameterByName.ts
Normal file
12
src/components/AdSlot/adScripts/getParameterByName.ts
Normal 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, ' '));
|
||||
};
|
||||
28
src/components/AdSlot/adScripts/ias.ts
Normal file
28
src/components/AdSlot/adScripts/ias.ts
Normal 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');
|
||||
};
|
||||
});
|
||||
};
|
||||
17
src/components/AdSlot/adScripts/loadScript.ts
Normal file
17
src/components/AdSlot/adScripts/loadScript.ts
Normal 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);
|
||||
};
|
||||
6
src/components/AdSlot/utils.ts
Normal file
6
src/components/AdSlot/utils.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
const random4 = () =>
|
||||
Math.floor((1 + Math.random()) * 0x10000)
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
|
||||
export const getRandomAdId = () => 'ad-' + random4() + random4();
|
||||
73
src/components/Analytics/Analytics.mdx
Normal file
73
src/components/Analytics/Analytics.mdx
Normal 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 />
|
||||
```
|
||||
17
src/components/Analytics/Analytics.stories.svelte
Normal file
17
src/components/Analytics/Analytics.stories.svelte
Normal 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' }],
|
||||
}}
|
||||
/>
|
||||
39
src/components/Analytics/Analytics.svelte
Normal file
39
src/components/Analytics/Analytics.svelte
Normal 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 />
|
||||
30
src/components/Analytics/GTM.svelte
Normal file
30
src/components/Analytics/GTM.svelte
Normal 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) -->
|
||||
23
src/components/Analytics/providers/chartbeat.ts
Normal file
23
src/components/Analytics/providers/chartbeat.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
24
src/components/Analytics/providers/ga.ts
Normal file
24
src/components/Analytics/providers/ga.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
2
src/components/Analytics/providers/index.ts
Normal file
2
src/components/Analytics/providers/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { default as ga } from './ga';
|
||||
export { default as chartbeat } from './chartbeat';
|
||||
117
src/components/Article/Article.mdx
Normal file
117
src/components/Article/Article.mdx
Normal 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} />
|
||||
117
src/components/Article/Article.stories.svelte
Normal file
117
src/components/Article/Article.stories.svelte
Normal 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>
|
||||
70
src/components/Article/Article.svelte
Normal file
70
src/components/Article/Article.svelte
Normal 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>
|
||||
111
src/components/BeforeAfter/BeforeAfter.mdx
Normal file
111
src/components/BeforeAfter/BeforeAfter.mdx
Normal 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} />
|
||||
68
src/components/BeforeAfter/BeforeAfter.stories.svelte
Normal file
68
src/components/BeforeAfter/BeforeAfter.stories.svelte
Normal 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>
|
||||
376
src/components/BeforeAfter/BeforeAfter.svelte
Normal file
376
src/components/BeforeAfter/BeforeAfter.svelte
Normal 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>
|
||||
BIN
src/components/BeforeAfter/images/myrne-after.jpg
Normal file
BIN
src/components/BeforeAfter/images/myrne-after.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 715 KiB |
BIN
src/components/BeforeAfter/images/myrne-before.jpg
Normal file
BIN
src/components/BeforeAfter/images/myrne-before.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 472 KiB |
99
src/components/Block/Block.mdx
Normal file
99
src/components/Block/Block.mdx
Normal 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>
|
||||
```
|
||||
181
src/components/Block/Block.stories.svelte
Normal file
181
src/components/Block/Block.stories.svelte
Normal 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>
|
||||
82
src/components/Block/Block.svelte
Normal file
82
src/components/Block/Block.svelte
Normal 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>
|
||||
115
src/components/BodyText/BodyText.mdx
Normal file
115
src/components/BodyText/BodyText.mdx
Normal 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;
|
||||
}
|
||||
}
|
||||
```
|
||||
102
src/components/BodyText/BodyText.stories.svelte
Normal file
102
src/components/BodyText/BodyText.stories.svelte
Normal 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>
|
||||
19
src/components/BodyText/BodyText.svelte
Normal file
19
src/components/BodyText/BodyText.svelte
Normal 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>
|
||||
110
src/components/Byline/Byline.mdx
Normal file
110
src/components/Byline/Byline.mdx
Normal 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} />
|
||||
````
|
||||
63
src/components/Byline/Byline.stories.svelte
Normal file
63
src/components/Byline/Byline.stories.svelte
Normal 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`;
|
||||
},
|
||||
}}
|
||||
/>
|
||||
188
src/components/Byline/Byline.svelte
Normal file
188
src/components/Byline/Byline.svelte
Normal 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 {/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))} {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())
|
||||
)} {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>
|
||||
45
src/components/DatawrapperChart/DatawrapperChart.mdx
Normal file
45
src/components/DatawrapperChart/DatawrapperChart.mdx
Normal 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} />
|
||||
|
|
@ -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',
|
||||
}}
|
||||
/>
|
||||
117
src/components/DatawrapperChart/DatawrapperChart.svelte
Normal file
117
src/components/DatawrapperChart/DatawrapperChart.svelte
Normal 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>
|
||||
26
src/components/DocumentCloud/DocumentCloud.mdx
Normal file
26
src/components/DocumentCloud/DocumentCloud.mdx
Normal 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} />
|
||||
23
src/components/DocumentCloud/DocumentCloud.stories.svelte
Normal file
23
src/components/DocumentCloud/DocumentCloud.stories.svelte
Normal 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',
|
||||
}}
|
||||
/>
|
||||
41
src/components/DocumentCloud/DocumentCloud.svelte
Normal file
41
src/components/DocumentCloud/DocumentCloud.svelte
Normal 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&responsive=1&title=1"
|
||||
title={altText}
|
||||
sandbox="allow-scripts allow-same-origin allow-popups allow-forms allow-popups-to-escape-sandbox"
|
||||
></iframe>
|
||||
</Block>
|
||||
19
src/components/EmbedPreviewerLink/EmbedPreviewerLink.mdx
Normal file
19
src/components/EmbedPreviewerLink/EmbedPreviewerLink.mdx
Normal 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} />
|
||||
```
|
||||
|
|
@ -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,
|
||||
}}
|
||||
/>
|
||||
32
src/components/EmbedPreviewerLink/EmbedPreviewerLink.svelte
Normal file
32
src/components/EmbedPreviewerLink/EmbedPreviewerLink.svelte
Normal 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>
|
||||
67
src/components/EndNotes/EndNotes.mdx
Normal file
67
src/components/EndNotes/EndNotes.mdx
Normal 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} />
|
||||
29
src/components/EndNotes/EndNotes.stories.svelte
Normal file
29
src/components/EndNotes/EndNotes.stories.svelte
Normal 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 }} />
|
||||
58
src/components/EndNotes/EndNotes.svelte
Normal file
58
src/components/EndNotes/EndNotes.svelte
Normal 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>
|
||||
72
src/components/FeaturePhoto/FeaturePhoto.mdx
Normal file
72
src/components/FeaturePhoto/FeaturePhoto.mdx
Normal 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} />
|
||||
41
src/components/FeaturePhoto/FeaturePhoto.stories.svelte
Normal file
41
src/components/FeaturePhoto/FeaturePhoto.stories.svelte
Normal 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',
|
||||
}}
|
||||
/>
|
||||
145
src/components/FeaturePhoto/FeaturePhoto.svelte
Normal file
145
src/components/FeaturePhoto/FeaturePhoto.svelte
Normal 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
Loading…
Reference in a new issue