Bump RGC to v3.3.2
This commit is contained in:
parent
9b8ddaadcd
commit
4255d7ce44
24 changed files with 1589 additions and 11 deletions
|
|
@ -1,5 +1,23 @@
|
||||||
# @reuters-graphics/graphics-components
|
# @reuters-graphics/graphics-components
|
||||||
|
|
||||||
|
## 3.3.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- d3c501d: Switch casting as GeoJSON to data property
|
||||||
|
|
||||||
|
## 3.3.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- b924478: Updates to blog component styles
|
||||||
|
|
||||||
|
## 3.3.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 8abe183: Adds graphics blog components
|
||||||
|
|
||||||
## 3.2.1
|
## 3.2.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@reuters-graphics/graphics-components",
|
"name": "@reuters-graphics/graphics-components",
|
||||||
"version": "3.2.1",
|
"version": "3.3.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": false,
|
"private": false,
|
||||||
"homepage": "https://reuters-graphics.github.io/graphics-components",
|
"homepage": "https://reuters-graphics.github.io/graphics-components",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { Meta, Canvas } from '@storybook/blocks';
|
||||||
|
|
||||||
|
import * as BlogPostStories from './BlogPost.stories.svelte';
|
||||||
|
|
||||||
|
<Meta of={BlogPostStories} />
|
||||||
|
|
||||||
|
# BlogPost
|
||||||
|
|
||||||
|
The `BlogPost` component renders a single entry in a graphics blog page, including a headline, dateline, authors and a divider. Body content is passed as children.
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script>
|
||||||
|
import { BlogPost } from '@reuters-graphics/graphics-components';
|
||||||
|
import { base } from '$app/paths';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<BlogPost
|
||||||
|
title="Iran fires ballistic missiles at Israel in major escalation"
|
||||||
|
slugTitle="Iran fires ballistic missiles at Israel in major escalation"
|
||||||
|
authors={['John Smith', 'Jane Doe']}
|
||||||
|
publishTime="2024-10-01T18:30:00Z"
|
||||||
|
updateTime="2024-10-01T21:45:00Z"
|
||||||
|
{base}
|
||||||
|
>
|
||||||
|
<!-- Post body content goes here -->
|
||||||
|
</BlogPost>
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Important:** Do not change `slugTitle` after publishing. It is used to generate the post's anchor link — changing it will break any published links to the post.
|
||||||
|
|
||||||
|
<Canvas of={BlogPostStories.Demo} />
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<script module lang="ts">
|
||||||
|
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||||
|
import BlogPost from './BlogPost.svelte';
|
||||||
|
import BodyText from '../BodyText/BodyText.svelte';
|
||||||
|
|
||||||
|
const { Story } = defineMeta({
|
||||||
|
title: 'Components/Blog/BlogPost',
|
||||||
|
component: BlogPost,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Story name="Demo">
|
||||||
|
<BlogPost
|
||||||
|
title="Iran fires ballistic missiles at Israel in major escalation"
|
||||||
|
slugTitle="Iran fires ballistic missiles at Israel in major escalation"
|
||||||
|
authors={['John Smith', 'Jane Doe']}
|
||||||
|
publishTime="2024-10-01T18:30:00Z"
|
||||||
|
updateTime="2024-10-01T21:45:00Z"
|
||||||
|
base=""
|
||||||
|
>
|
||||||
|
<BodyText
|
||||||
|
text="Iran launched a barrage of ballistic missiles at Israel on Tuesday in its first direct attack on Israeli territory, marking a significant escalation in the conflict gripping the Middle East."
|
||||||
|
/>
|
||||||
|
<BodyText
|
||||||
|
text="The attack, which Iran said was in retaliation for Israeli strikes that killed senior Hezbollah and Hamas leaders, prompted Israel and the United States to vow a response."
|
||||||
|
/>
|
||||||
|
</BlogPost>
|
||||||
|
</Story>
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
<script lang="ts">
|
||||||
|
interface Props {
|
||||||
|
/** Title of the blog post */
|
||||||
|
title: string;
|
||||||
|
/**
|
||||||
|
* A sluggable title of the blog post.
|
||||||
|
*
|
||||||
|
* **Important:** Do not change this title after publishing the post. Changes will break
|
||||||
|
* published links to the post.
|
||||||
|
*/
|
||||||
|
slugTitle: string;
|
||||||
|
/**
|
||||||
|
* Base path prepended to the copied URL, e.g. "/graphics", usually gotten from base store in SvelteKit.
|
||||||
|
*/
|
||||||
|
base: string;
|
||||||
|
/** 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;
|
||||||
|
/** Add an id to target post headline with custom CSS. */
|
||||||
|
id?: string;
|
||||||
|
/** Add extra classes to target post headline with custom CSS. */
|
||||||
|
cls?: string;
|
||||||
|
/**
|
||||||
|
* If the post is the last on the page, remove the dividing rule used to separate posts.
|
||||||
|
*/
|
||||||
|
isLastPost?: boolean;
|
||||||
|
children?: import('svelte').Snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
title = 'Reuters Graphics blog post',
|
||||||
|
slugTitle = 'Reuters Graphics blog post',
|
||||||
|
base = '',
|
||||||
|
authors = [],
|
||||||
|
publishTime = '',
|
||||||
|
updateTime = '',
|
||||||
|
id = '',
|
||||||
|
cls = '',
|
||||||
|
isLastPost = false,
|
||||||
|
children,
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
import PostHeadline from './PostHeadline.svelte';
|
||||||
|
import { slugify } from '../../utils';
|
||||||
|
import { getShortDate } from './utils';
|
||||||
|
|
||||||
|
let shortPubDate = $derived(getShortDate(publishTime));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<article id={slugify(slugTitle)} class="post-anchor">
|
||||||
|
<PostHeadline
|
||||||
|
hed={title}
|
||||||
|
sluggableHed={slugTitle}
|
||||||
|
{base}
|
||||||
|
{authors}
|
||||||
|
{publishTime}
|
||||||
|
{updateTime}
|
||||||
|
{id}
|
||||||
|
{cls}
|
||||||
|
/>
|
||||||
|
<a href="{base}/{shortPubDate}/{slugify(slugTitle)}/" hidden>
|
||||||
|
{title}
|
||||||
|
</a>
|
||||||
|
{@render children?.()}
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{#if !isLastPost}
|
||||||
|
<div class="divider"></div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '../../scss/mixins' as mixins;
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
height: 1px;
|
||||||
|
max-width: calc(mixins.$column-width-normal * 0.9);
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
margin: 4rem auto;
|
||||||
|
border-top: solid 1px var(--tr-muted-grey);
|
||||||
|
}
|
||||||
|
.post-anchor {
|
||||||
|
scroll-margin-top: 125px;
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
scroll-margin-top: 150px;
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
scroll-margin-top: 175px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Fa from 'svelte-fa';
|
||||||
|
import { slugify } from '../../utils';
|
||||||
|
import { getShortDate } from './utils';
|
||||||
|
import { faLink } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** The published date of the post. */
|
||||||
|
publishedDate?: string;
|
||||||
|
/** The heading of the post. */
|
||||||
|
hed?: string;
|
||||||
|
/** Base path prepended to the copied URL, e.g. "/graphics". */
|
||||||
|
base: string;
|
||||||
|
/** The ARIA label for the copy link button. */
|
||||||
|
ariaLabel?: string;
|
||||||
|
/** The message to display before copying the link. */
|
||||||
|
copyMessageBefore?: string;
|
||||||
|
/** The message to display after copying the link. */
|
||||||
|
copyMessageAfter?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
publishedDate = '',
|
||||||
|
hed = '',
|
||||||
|
base = '',
|
||||||
|
ariaLabel = 'Copy link to this post',
|
||||||
|
copyMessageBefore = 'Copy link',
|
||||||
|
copyMessageAfter = 'Copied',
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
let hovering = $state(false);
|
||||||
|
let clicked = $state(false);
|
||||||
|
|
||||||
|
let publishDate = $derived(getShortDate(publishedDate));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="link-container pl-0.5">
|
||||||
|
<div class="mask"></div>
|
||||||
|
<button
|
||||||
|
class:clicked
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
role="link"
|
||||||
|
onclick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
clicked = true;
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
`${window.location.origin}${base}/${publishDate}/${slugify(hed)}/`
|
||||||
|
);
|
||||||
|
setTimeout(() => {
|
||||||
|
clicked = false;
|
||||||
|
}, 2000);
|
||||||
|
}}
|
||||||
|
onmouseenter={() => {
|
||||||
|
hovering = true;
|
||||||
|
}}
|
||||||
|
onmouseleave={() => {
|
||||||
|
hovering = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Fa icon={faLink} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if !clicked}
|
||||||
|
<div class="message" class:active={hovering}>{copyMessageBefore}</div>
|
||||||
|
{:else}
|
||||||
|
<div class="message" class:active={clicked}>{copyMessageAfter}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '../../scss/mixins' as mixins;
|
||||||
|
|
||||||
|
.link-container {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-items: center;
|
||||||
|
align-items: center;
|
||||||
|
overflow-y: hidden;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.message {
|
||||||
|
background-color: var(--theme-colour-text-primary);
|
||||||
|
color: var(--theme-colour-background);
|
||||||
|
@include mixins.text-xxs;
|
||||||
|
font-weight: 500;
|
||||||
|
@include mixins.fpl-3;
|
||||||
|
@include mixins.fpr-2;
|
||||||
|
@include mixins.leading-loose;
|
||||||
|
display: inline-flex;
|
||||||
|
border-top-right-radius: 20px;
|
||||||
|
border-bottom-right-radius: 20px;
|
||||||
|
transform: translateX(-100px);
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
opacity: 0;
|
||||||
|
&.active {
|
||||||
|
transform: translateX(-10px);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.mask {
|
||||||
|
background-color: var(--theme-colour-background);
|
||||||
|
display: inline-block;
|
||||||
|
height: 30px;
|
||||||
|
width: 20px;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
z-index: 1;
|
||||||
|
background-color: var(--theme-colour-background);
|
||||||
|
border-radius: 50%;
|
||||||
|
@include mixins.text-sm;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
display: inline-flex;
|
||||||
|
justify-items: center;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid var(--theme-colour-background);
|
||||||
|
color: var(--tr-light-grey);
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover,
|
||||||
|
&.clicked {
|
||||||
|
color: var(--tr-dark-grey);
|
||||||
|
border: 1px solid var(--tr-dark-grey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { apdate } from 'journalize';
|
||||||
|
import CopyLink from './CopyLink.svelte';
|
||||||
|
import { slugify } from '../../utils';
|
||||||
|
import Kinesis from '../KinesisLogo/KinesisLogo.svelte';
|
||||||
|
import Block from '../Block/Block.svelte';
|
||||||
|
import Byline from '../Byline/Byline.svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** Headline of the blog post */
|
||||||
|
hed: string;
|
||||||
|
/**
|
||||||
|
* A sluggable headline of the blog post.
|
||||||
|
*
|
||||||
|
* **Important:** Do not change this headline after publishing the post. Changes will break
|
||||||
|
* published links to the post.
|
||||||
|
*/
|
||||||
|
sluggableHed: string;
|
||||||
|
/** Base path prepended to the copied URL, e.g. "/graphics". */
|
||||||
|
base: string;
|
||||||
|
/** 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;
|
||||||
|
/** Add an id to target with custom CSS. */
|
||||||
|
id?: string;
|
||||||
|
/** Add extra classes to target with custom CSS. */
|
||||||
|
cls?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
hed = 'Reuters Graphics blog post',
|
||||||
|
sluggableHed = 'Reuters Graphics blog post',
|
||||||
|
base = '',
|
||||||
|
authors = [],
|
||||||
|
publishTime = '',
|
||||||
|
updateTime = '',
|
||||||
|
id = '',
|
||||||
|
cls = '',
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
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: 'numeric',
|
||||||
|
minute: '2-digit',
|
||||||
|
timeZoneName: 'short',
|
||||||
|
});
|
||||||
|
|
||||||
|
const areSameDay = (first: Date, second: Date) =>
|
||||||
|
first.getFullYear() === second.getFullYear() &&
|
||||||
|
first.getMonth() === second.getMonth() &&
|
||||||
|
first.getDate() === second.getDate();
|
||||||
|
|
||||||
|
let hedMain = $derived(hed.split(' ').slice(0, -1).join(' '));
|
||||||
|
let hedWidow = $derived(hed.split(' ').slice(-1).join(' '));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Block {id} class="headline-container post-headline fmt-7 {cls}" width="normal">
|
||||||
|
<header class="headline">
|
||||||
|
<div class="kinesis">
|
||||||
|
<Kinesis width="22px" colour="#fff" />
|
||||||
|
</div>
|
||||||
|
<div class="dateline-container">
|
||||||
|
{#if isValidDate(publishTime)}
|
||||||
|
<div class="published-time font-bold whitespace-nowrap inline-block">
|
||||||
|
<time class="uppercase" datetime={publishTime}>
|
||||||
|
{#if isValidDate(updateTime)}
|
||||||
|
{apdate(new Date(publishTime))}
|
||||||
|
{:else}
|
||||||
|
{apdate(new Date(publishTime))} {formatTime(
|
||||||
|
publishTime
|
||||||
|
)}
|
||||||
|
{/if}
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if isValidDate(publishTime) && isValidDate(updateTime)}
|
||||||
|
<div class="updated-time font-bold whitespace-nowrap block">
|
||||||
|
<span>Updated</span>
|
||||||
|
<time class="uppercase" datetime={updateTime}>
|
||||||
|
{#if areSameDay(new Date(publishTime), new Date(updateTime))}
|
||||||
|
{formatTime(updateTime)}
|
||||||
|
{:else}
|
||||||
|
{apdate(new Date(updateTime))} {formatTime(updateTime)}
|
||||||
|
{/if}
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="title">
|
||||||
|
{#if hed}
|
||||||
|
<h2>
|
||||||
|
<a href="#{slugify(sluggableHed)}">
|
||||||
|
{hedMain}
|
||||||
|
<span
|
||||||
|
>{hedWidow}
|
||||||
|
<CopyLink
|
||||||
|
hed={sluggableHed}
|
||||||
|
publishedDate={publishTime}
|
||||||
|
{base}
|
||||||
|
/></span
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
</h2>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<Byline {authors} />
|
||||||
|
</header>
|
||||||
|
</Block>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '../../scss/mixins' as mixins;
|
||||||
|
|
||||||
|
.headline {
|
||||||
|
position: relative;
|
||||||
|
.kinesis {
|
||||||
|
position: absolute;
|
||||||
|
top: -7px;
|
||||||
|
left: -39px;
|
||||||
|
background-color: #d64000;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
@media (max-width: 820px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dateline-container {
|
||||||
|
@include mixins.body-caption;
|
||||||
|
@include mixins.fmy-0;
|
||||||
|
text-align: left;
|
||||||
|
color: var(--theme-colour-accent);
|
||||||
|
|
||||||
|
@media (min-width: mixins.$column-width-narrow) {
|
||||||
|
.updated-time {
|
||||||
|
display: inline-block;
|
||||||
|
&:before {
|
||||||
|
content: '·';
|
||||||
|
margin: 0 5px 0 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
@include mixins.fmy-1;
|
||||||
|
@include mixins.font-black;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
white-space: nowrap;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
/**
|
||||||
|
* Converts a date string to a short date format.
|
||||||
|
* @param d - The date string to be converted.
|
||||||
|
* @returns The short date format string.
|
||||||
|
*/
|
||||||
|
export const getShortDate = (d: string) =>
|
||||||
|
new Date(d).toISOString().split('T')[0];
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { Meta, Canvas } from '@storybook/blocks';
|
||||||
|
|
||||||
|
import * as BlogTOCStories from './BlogTOC.stories.svelte';
|
||||||
|
|
||||||
|
<Meta of={BlogTOCStories} />
|
||||||
|
|
||||||
|
# BlogTOC
|
||||||
|
|
||||||
|
The `BlogTOC` component renders a collapsible table of contents for a graphics blog page, listing all posts sorted chronologically. It only renders when there are two or more posts.
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script>
|
||||||
|
import { BlogTOC } from '@reuters-graphics/graphics-components';
|
||||||
|
import { base } from '$app/paths';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<BlogTOC
|
||||||
|
posts={[
|
||||||
|
{
|
||||||
|
title: 'Iran fires ballistic missiles at Israel',
|
||||||
|
slugTitle: 'Iran fires ballistic missiles at Israel',
|
||||||
|
publishTime: '2024-10-01T18:30:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Israel vows response',
|
||||||
|
slugTitle: 'Israel vows response',
|
||||||
|
publishTime: '2024-10-02T09:15:00Z',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
{base}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `base` prop should be set to your SvelteKit `base` path (e.g. `"/graphics"`) so that post links resolve correctly.
|
||||||
|
|
||||||
|
Each post in the `posts` array must have:
|
||||||
|
|
||||||
|
- `title` — the display title
|
||||||
|
- `slugTitle` — used to generate the anchor link; **do not change after publishing**
|
||||||
|
- `publishTime` — an ISO datetime string used for sorting and the dateline
|
||||||
|
|
||||||
|
<Canvas of={BlogTOCStories.Demo} />
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
<script module lang="ts">
|
||||||
|
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||||
|
import BlogTOC from './BlogTOC.svelte';
|
||||||
|
import BlogPost from '../BlogPost/BlogPost.svelte';
|
||||||
|
import BodyText from '../BodyText/BodyText.svelte';
|
||||||
|
import Headline from '../Headline/Headline.svelte';
|
||||||
|
import ClockWall from '../ClockWall/ClockWall.svelte';
|
||||||
|
|
||||||
|
const { Story } = defineMeta({
|
||||||
|
title: 'Components/Blog/BlogTOC',
|
||||||
|
component: BlogTOC,
|
||||||
|
});
|
||||||
|
|
||||||
|
const posts = [
|
||||||
|
{
|
||||||
|
title: 'Iran fires ballistic missiles at Israel in major escalation',
|
||||||
|
slugTitle: 'Iran fires ballistic missiles at Israel in major escalation',
|
||||||
|
publishTime: '2024-10-01T18:30:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Israel vows response as world leaders call for restraint',
|
||||||
|
slugTitle: 'Israel vows response as world leaders call for restraint',
|
||||||
|
publishTime: '2024-10-02T09:15:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Oil prices surge amid fears of wider Middle East conflict',
|
||||||
|
slugTitle: 'Oil prices surge amid fears of wider Middle East conflict',
|
||||||
|
publishTime: '2024-10-02T14:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title:
|
||||||
|
'UN Security Council holds emergency session on Iran-Israel crisis',
|
||||||
|
slugTitle:
|
||||||
|
'UN Security Council holds emergency session on Iran-Israel crisis',
|
||||||
|
publishTime: '2024-10-03T11:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Iran says missile attack achieved its objectives',
|
||||||
|
slugTitle: 'Iran says missile attack achieved its objectives',
|
||||||
|
publishTime: '2024-10-03T16:30:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Israel launches airstrikes on Hezbollah targets in Lebanon',
|
||||||
|
slugTitle: 'Israel launches airstrikes on Hezbollah targets in Lebanon',
|
||||||
|
publishTime: '2024-10-04T08:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'U.S. sends additional warships to Middle East region',
|
||||||
|
slugTitle: 'U.S. sends additional warships to Middle East region',
|
||||||
|
publishTime: '2024-10-04T13:45:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Story name="Demo" args={{ posts, base: '' }}>
|
||||||
|
<Headline
|
||||||
|
section="Graphics"
|
||||||
|
hed="Maps of the Iran crisis"
|
||||||
|
hedSize="big"
|
||||||
|
width="normal"
|
||||||
|
class="mb-2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ClockWall
|
||||||
|
cities={[
|
||||||
|
{ name: 'Tehran', tzIdentifier: 'Asia/Tehran' },
|
||||||
|
{ name: 'Tel Aviv', tzIdentifier: 'Asia/Tel_Aviv' },
|
||||||
|
{ name: 'Washington D.C.', tzIdentifier: 'America/New_York' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<BlogTOC {posts} base="" />
|
||||||
|
|
||||||
|
<BlogPost
|
||||||
|
title="Iran fires ballistic missiles at Israel in major escalation"
|
||||||
|
slugTitle="Iran fires ballistic missiles at Israel in major escalation"
|
||||||
|
authors={['John Smith', 'Jane Doe']}
|
||||||
|
publishTime="2024-10-01T18:30:00Z"
|
||||||
|
updateTime="2024-10-01T21:45:00Z"
|
||||||
|
base=""
|
||||||
|
>
|
||||||
|
<BodyText
|
||||||
|
text="Iran launched a barrage of ballistic missiles at Israel on Tuesday in its first direct attack on Israeli territory, marking a significant escalation in the conflict gripping the Middle East."
|
||||||
|
/>
|
||||||
|
<BodyText
|
||||||
|
text="The attack, which Iran said was in retaliation for Israeli strikes that killed senior Hezbollah and Hamas leaders, prompted Israel and the United States to vow a response."
|
||||||
|
/>
|
||||||
|
</BlogPost>
|
||||||
|
</Story>
|
||||||
|
|
@ -0,0 +1,275 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Block from '../Block/Block.svelte';
|
||||||
|
import TOCList from './TOCList.svelte';
|
||||||
|
import { apmonth } from 'journalize';
|
||||||
|
import { slugify } from '../../utils';
|
||||||
|
import { slide } from 'svelte/transition';
|
||||||
|
import Fa from 'svelte-fa';
|
||||||
|
import {
|
||||||
|
faCaretDown,
|
||||||
|
faAngleDoubleUp,
|
||||||
|
faAngleDoubleDown,
|
||||||
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
interface Post {
|
||||||
|
title: string;
|
||||||
|
slugTitle: string;
|
||||||
|
publishTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
posts: Post[];
|
||||||
|
/** Base path prepended to post links, e.g. "/graphics". */
|
||||||
|
base: string;
|
||||||
|
/** The label for the table of contents toggle button. */
|
||||||
|
label?: string;
|
||||||
|
/** The maximum height of the table of contents list in pixels. */
|
||||||
|
maxHeight?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
posts,
|
||||||
|
base = '',
|
||||||
|
label = 'Show all articles',
|
||||||
|
maxHeight = 300,
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
let showContents = $state(false);
|
||||||
|
let scrollPos = $state(0);
|
||||||
|
let listHeight = $state(0);
|
||||||
|
|
||||||
|
const contents = $derived(
|
||||||
|
[...posts]
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
new Date(a.publishTime).getTime() - new Date(b.publishTime).getTime()
|
||||||
|
)
|
||||||
|
.reduce(
|
||||||
|
(acc, post) => {
|
||||||
|
const d = new Date(post.publishTime);
|
||||||
|
const dateKey = `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
|
||||||
|
const existing = acc.find((entry) => entry.dateKey === dateKey);
|
||||||
|
const event = {
|
||||||
|
title: post.title,
|
||||||
|
titleLink: `${base}/#${slugify(post.slugTitle)}`,
|
||||||
|
};
|
||||||
|
if (existing) {
|
||||||
|
existing.events.push(event);
|
||||||
|
} else {
|
||||||
|
acc.push({
|
||||||
|
dateKey,
|
||||||
|
date: `${apmonth(d)} ${d.getDate()}`,
|
||||||
|
events: [event],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[] as {
|
||||||
|
dateKey: string;
|
||||||
|
date: string;
|
||||||
|
events: { title: string; titleLink: string }[];
|
||||||
|
}[]
|
||||||
|
)
|
||||||
|
.map(({ dateKey: _dateKey, ...rest }) => rest)
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if contents.length > 1}
|
||||||
|
<Block width="fluid" class="mt-4 mb-0">
|
||||||
|
<div class="toc-gutter">
|
||||||
|
<div class="table-of-contents" style="--mh: {maxHeight}px;">
|
||||||
|
<div class="flex w-full">
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
showContents = !showContents;
|
||||||
|
scrollPos = 0;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="icon" class:expanded={showContents}>
|
||||||
|
<Fa icon={faCaretDown} size="lg" />
|
||||||
|
</div>
|
||||||
|
<div class="label text-xs leading-loose tracking-wide py-0.5">
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Block
|
||||||
|
width="normal"
|
||||||
|
class="my-0 ml-2 relative {showContents ? 'fpb-6' : ''}"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{#if showContents}
|
||||||
|
<div
|
||||||
|
class="content-container fmt-3"
|
||||||
|
transition:slide={{ axis: 'y', duration: 350 }}
|
||||||
|
onscroll={(e) => {
|
||||||
|
scrollPos = e.currentTarget.scrollTop;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TOCList dates={contents} bind:listHeight />
|
||||||
|
|
||||||
|
{#if scrollPos > 10 && listHeight > maxHeight}
|
||||||
|
<div class="scroll-icon up">
|
||||||
|
<Fa icon={faAngleDoubleUp} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if listHeight > maxHeight && scrollPos < 0.95 * (listHeight - maxHeight)}
|
||||||
|
<div class="scroll-icon down">
|
||||||
|
<Fa icon={faAngleDoubleDown} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div></Block
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Block>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '../../scss/mixins' as mixins;
|
||||||
|
|
||||||
|
.toc-gutter {
|
||||||
|
// background-color: #fafafa;
|
||||||
|
// border-top: 1px solid #dedede;
|
||||||
|
// border-bottom: 1px solid #dedede;
|
||||||
|
padding: 5px 0 0;
|
||||||
|
margin-bottom: -40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-of-contents {
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: var(--normal-column-width);
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
& > div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.content-container {
|
||||||
|
max-height: var(--mh);
|
||||||
|
overflow-y: auto;
|
||||||
|
scroll-snap-type: y mandatory;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-icon {
|
||||||
|
position: absolute;
|
||||||
|
margin-left: -4px;
|
||||||
|
color: var(--theme-colour-text-secondary);
|
||||||
|
background-color: var(--theme-colour-background);
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&.up {
|
||||||
|
top: 0px;
|
||||||
|
animation: fade_scroll_up 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.down {
|
||||||
|
bottom: 30px;
|
||||||
|
animation: fade_scroll_down 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes fade_scroll_up {
|
||||||
|
0% {
|
||||||
|
transform: translate(0, 5px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(0, -10px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes fade_scroll_down {
|
||||||
|
0% {
|
||||||
|
transform: translate(0, -10px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(0, 5px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
display: inline-flex;
|
||||||
|
font-family: var(--theme-font-family-hed);
|
||||||
|
font-weight: normal;
|
||||||
|
padding: 0 5px;
|
||||||
|
color: var(--theme-colour-accent);
|
||||||
|
@include mixins.text-sm;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid var(--tr-muted-grey);
|
||||||
|
border-radius: 50px;
|
||||||
|
&:hover {
|
||||||
|
border: 1px solid var(--theme-colour-text-secondary);
|
||||||
|
background-color: #efefef;
|
||||||
|
div.label {
|
||||||
|
color: var(--theme-colour-text-primary);
|
||||||
|
}
|
||||||
|
div.icon {
|
||||||
|
color: var(--theme-colour-text-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.icon {
|
||||||
|
z-index: 1;
|
||||||
|
@include mixins.text-sm;
|
||||||
|
@include mixins.leading-loose;
|
||||||
|
width: 18px;
|
||||||
|
height: 24px;
|
||||||
|
margin-left: 6px;
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--tr-light-grey);
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
|
||||||
|
&.expanded {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div.label {
|
||||||
|
color: var(--theme-colour-text-secondary);
|
||||||
|
display: inline-flex;
|
||||||
|
font-weight: 500;
|
||||||
|
@include mixins.fpl-4;
|
||||||
|
@include mixins.fpr-2;
|
||||||
|
margin-left: -15px;
|
||||||
|
position: relative;
|
||||||
|
border-top-right-radius: 20px;
|
||||||
|
border-bottom-right-radius: 20px;
|
||||||
|
font-style: italic;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0.2;
|
||||||
|
border-top-right-radius: 20px;
|
||||||
|
border-bottom-right-radius: 20px;
|
||||||
|
// background-color: var(--theme-colour-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Block from '../Block/Block.svelte';
|
||||||
|
|
||||||
|
interface DateEvent {
|
||||||
|
title: string;
|
||||||
|
titleLink: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DateEntry {
|
||||||
|
date: string;
|
||||||
|
events: DateEvent[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
dates: DateEntry[];
|
||||||
|
/** Colour for the timeline bullet symbols and line. */
|
||||||
|
symbolColour?: string;
|
||||||
|
/** Colour for the date headings. */
|
||||||
|
dateColour?: string;
|
||||||
|
/** The height of the list, bindable for the parent to read. */
|
||||||
|
listHeight?: number;
|
||||||
|
id?: string;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
dates,
|
||||||
|
symbolColour = 'var(--theme-colour-brand-rules)',
|
||||||
|
dateColour = 'var(--theme-colour-accent, red)',
|
||||||
|
listHeight = $bindable(0),
|
||||||
|
id = '',
|
||||||
|
class: cls = '',
|
||||||
|
}: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Block width="normal" {id} class="simple-timeline-container {cls}">
|
||||||
|
<div
|
||||||
|
bind:clientHeight={listHeight}
|
||||||
|
class="timeline"
|
||||||
|
style="--symbol-colour:{symbolColour};"
|
||||||
|
>
|
||||||
|
{#each dates as date}
|
||||||
|
<div class="date">
|
||||||
|
<svg class="absolute bg" height="25" width="20">
|
||||||
|
<circle
|
||||||
|
cx="10"
|
||||||
|
cy="12"
|
||||||
|
r="5"
|
||||||
|
stroke={symbolColour}
|
||||||
|
stroke-width="2"
|
||||||
|
fill="transparent"
|
||||||
|
></circle>
|
||||||
|
</svg>
|
||||||
|
<div class="timeline-date" style:color={dateColour}>
|
||||||
|
{date.date}
|
||||||
|
</div>
|
||||||
|
{#each date.events as event}
|
||||||
|
<div class="event">
|
||||||
|
<a href={event.titleLink}>
|
||||||
|
<div class="title">{event.title}</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</Block>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '../../scss/mixins' as mixins;
|
||||||
|
.timeline {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
padding-right: 0.875rem;
|
||||||
|
.date {
|
||||||
|
position: relative;
|
||||||
|
padding-top: 0.125rem;
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
scroll-snap-align: start;
|
||||||
|
border-left: 1px solid var(--symbol-colour);
|
||||||
|
&:last-child {
|
||||||
|
border-left: 1px solid mixins.$theme-colour-background;
|
||||||
|
@include mixins.fpb-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-date {
|
||||||
|
@include mixins.font-note;
|
||||||
|
@include mixins.text-xxs;
|
||||||
|
text-transform: uppercase;
|
||||||
|
@include mixins.font-semibold;
|
||||||
|
@include mixins.tracking-wide;
|
||||||
|
@include mixins.fmb-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
top: -1px;
|
||||||
|
left: -10.5px;
|
||||||
|
}
|
||||||
|
div.title {
|
||||||
|
@include mixins.h3;
|
||||||
|
@include mixins.text-base;
|
||||||
|
@include mixins.fmy-1;
|
||||||
|
@include mixins.font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.event {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:global(p) {
|
||||||
|
@include mixins.body-note;
|
||||||
|
@include mixins.font-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
/**
|
/**
|
||||||
* Publish time as a datetime string.
|
* Publish time as a datetime string.
|
||||||
*/
|
*/
|
||||||
publishTime: string;
|
publishTime?: string;
|
||||||
/**
|
/**
|
||||||
* Update time as a datetime string.
|
* Update time as a datetime string.
|
||||||
*/
|
*/
|
||||||
|
|
@ -52,7 +52,7 @@
|
||||||
|
|
||||||
let {
|
let {
|
||||||
authors = [],
|
authors = [],
|
||||||
publishTime,
|
publishTime = '',
|
||||||
updateTime,
|
updateTime,
|
||||||
align = 'auto',
|
align = 'auto',
|
||||||
id = '',
|
id = '',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,264 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onDestroy, onMount } from 'svelte';
|
||||||
|
|
||||||
|
const CLOCK_WEIGHT = { Light: 1, Normal: 2, Bold: 4 } as const;
|
||||||
|
type ClockWeight = keyof typeof CLOCK_WEIGHT;
|
||||||
|
|
||||||
|
const CLOCK_SIZE = { XS: 48, MD: 80, LG: 120, XL: 160 } as const;
|
||||||
|
type ClockSize = keyof typeof CLOCK_SIZE;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/**
|
||||||
|
* The name of the clock (to be displayed), e.g. "New York"
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* The UTC time to display, defaults to current time
|
||||||
|
*/
|
||||||
|
UTCTime?: Date;
|
||||||
|
/**
|
||||||
|
* The timezone identifier, e.g. "America/New_York"
|
||||||
|
*/
|
||||||
|
tzIdentifier: string;
|
||||||
|
/**
|
||||||
|
* Whether to show the clock, defaults to true
|
||||||
|
*/
|
||||||
|
showClock?: boolean;
|
||||||
|
/**
|
||||||
|
* The weight of the clock, either "normal" or "bold"
|
||||||
|
*/
|
||||||
|
clockWeight?: ClockWeight;
|
||||||
|
/**
|
||||||
|
* The size of the clock, either "XS", "MD", "LG", or "XL"
|
||||||
|
*/
|
||||||
|
clockSize?: ClockSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
UTCTime = new Date(new Date().toUTCString()),
|
||||||
|
tzIdentifier,
|
||||||
|
showClock = true,
|
||||||
|
clockWeight = 'Normal',
|
||||||
|
clockSize = 'MD',
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a UTC date to a specified timezone and formats it to a.m./p.m. style.
|
||||||
|
*
|
||||||
|
* @param utcDate - The UTC date to convert.
|
||||||
|
* @param timezone - The timezone identifier.
|
||||||
|
* @returns The formatted time string.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function convertUTCToTimezone(utcDate: Date, timezone: string) {
|
||||||
|
const time = new Date(utcDate).toLocaleString('en-US', {
|
||||||
|
timeZone: timezone,
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert AM/PM to a.m./p.m. format
|
||||||
|
return time.replace('AM', 'a.m.').replace('PM', 'p.m.');
|
||||||
|
}
|
||||||
|
|
||||||
|
let clockInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
let time: string = $state(convertUTCToTimezone(UTCTime, tzIdentifier));
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
clockInterval = setInterval(() => {
|
||||||
|
time = convertUTCToTimezone(
|
||||||
|
new Date(new Date().toUTCString()),
|
||||||
|
tzIdentifier
|
||||||
|
);
|
||||||
|
}, 1000 * 10); // Update every 10 seconds
|
||||||
|
});
|
||||||
|
|
||||||
|
let minute: number = $derived(
|
||||||
|
parseFloat(time?.split(' ')[0].split(':')[1]) || 0
|
||||||
|
);
|
||||||
|
let hour: number = $derived(
|
||||||
|
parseFloat(time?.split(' ')[0].split(':')[0]) || 0
|
||||||
|
);
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (clockInterval) {
|
||||||
|
clearInterval(clockInterval);
|
||||||
|
clockInterval = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="clock-container" style="--clock-size: {CLOCK_SIZE[clockSize]}px;">
|
||||||
|
{#if showClock}
|
||||||
|
<svg class="clock-svg" width="100%" height="100%" viewBox="0 0 120 120">
|
||||||
|
<defs>
|
||||||
|
<filter id="inset-shadow">
|
||||||
|
<!-- Shadow offset -->
|
||||||
|
<feOffset dx="0" dy="4" />
|
||||||
|
<!-- Shadow blur -->
|
||||||
|
<feGaussianBlur stdDeviation="8" result="offset-blur" />
|
||||||
|
<!-- Invert drop shadow to make an inset shadow-->
|
||||||
|
<feComposite
|
||||||
|
operator="out"
|
||||||
|
in="SourceGraphic"
|
||||||
|
in2="offset-blur"
|
||||||
|
result="inverse"
|
||||||
|
/>
|
||||||
|
<!-- Cut colour inside shadow -->
|
||||||
|
<feFlood flood-color="black" flood-opacity=".2" result="color" />
|
||||||
|
<feComposite operator="in" in="color" in2="inverse" result="shadow" />
|
||||||
|
<!-- Placing shadow over element -->
|
||||||
|
<feComposite operator="over" in="shadow" in2="SourceGraphic" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<circle
|
||||||
|
class="clock-outer-border"
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
r="58"
|
||||||
|
fill="transparent"
|
||||||
|
stroke="#cccccc"
|
||||||
|
stroke-width="2"
|
||||||
|
></circle>
|
||||||
|
<circle
|
||||||
|
class="clock-inner-shadow"
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
r="54"
|
||||||
|
fill="#ffffff"
|
||||||
|
filter="url(#inset-shadow)"
|
||||||
|
></circle>
|
||||||
|
<g id="clock-ticks" style="mix-blend-mode: multiply;">
|
||||||
|
{#each Array(12) as _, i (i)}
|
||||||
|
<line
|
||||||
|
class="clock-hour-mark"
|
||||||
|
x1="50%"
|
||||||
|
y1="56"
|
||||||
|
x2="50%"
|
||||||
|
y2="64"
|
||||||
|
stroke="var(--tr-light-grey)"
|
||||||
|
stroke-width="2"
|
||||||
|
transform-origin="50% 50%"
|
||||||
|
transform="rotate({i * 30}) translate(0, -46)"
|
||||||
|
></line>
|
||||||
|
{/each}
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="clock-hand-minute"
|
||||||
|
transform-origin="50% 50%"
|
||||||
|
transform="rotate({(minute / 60) * 360})"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
r="4"
|
||||||
|
fill="transparent"
|
||||||
|
stroke="var(--tr-light-grey)"
|
||||||
|
stroke-width={CLOCK_WEIGHT[clockWeight]}
|
||||||
|
></circle>
|
||||||
|
<line
|
||||||
|
x1="50%"
|
||||||
|
y1={60 - 4 - 36}
|
||||||
|
x2="50%"
|
||||||
|
y2={60 - 4}
|
||||||
|
stroke="var(--tr-light-grey)"
|
||||||
|
stroke-width={CLOCK_WEIGHT[clockWeight]}
|
||||||
|
transform-origin="50% 50%"
|
||||||
|
></line>
|
||||||
|
<line
|
||||||
|
x1="50%"
|
||||||
|
y1={60 + 4}
|
||||||
|
x2="50%"
|
||||||
|
y2={60 + 4 + 4}
|
||||||
|
stroke="var(--tr-light-grey)"
|
||||||
|
stroke-width={CLOCK_WEIGHT[clockWeight]}
|
||||||
|
transform-origin="50% 50%"
|
||||||
|
></line>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="clock-hand-hour"
|
||||||
|
transform-origin="50% 50%"
|
||||||
|
transform="rotate({(hour / 12) * 360 + (360 / 12) * (minute / 60)})"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
r="4"
|
||||||
|
fill="transparent"
|
||||||
|
stroke="var(--tr-dark-grey)"
|
||||||
|
stroke-width={CLOCK_WEIGHT[clockWeight]}
|
||||||
|
></circle>
|
||||||
|
<line
|
||||||
|
x1="50%"
|
||||||
|
y1={60 - 4 - 24}
|
||||||
|
x2="50%"
|
||||||
|
y2={60 - 4}
|
||||||
|
stroke="var(--tr-dark-grey)"
|
||||||
|
stroke-width={CLOCK_WEIGHT[clockWeight]}
|
||||||
|
transform-origin="50% 50%"
|
||||||
|
></line>
|
||||||
|
<line
|
||||||
|
x1="50%"
|
||||||
|
y1={60 + 4}
|
||||||
|
x2="50%"
|
||||||
|
y2={60 + 4 + 4}
|
||||||
|
stroke="var(--tr-dark-grey)"
|
||||||
|
stroke-width={CLOCK_WEIGHT[clockWeight]}
|
||||||
|
transform-origin="50% 50%"
|
||||||
|
></line>
|
||||||
|
</g>
|
||||||
|
<circle
|
||||||
|
class="clock-origin"
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
r="2"
|
||||||
|
fill="var(--tr-dark-grey)"
|
||||||
|
></circle>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
<div class="clock-info">
|
||||||
|
<p class="m-0 p-0 font-sans font-medium leading-none text-sm">
|
||||||
|
{name}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="m-0 p-0 font-sans text-xs leading-none"
|
||||||
|
style="color: var(--tr-medium-grey);"
|
||||||
|
>
|
||||||
|
{time}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '../../scss/mixins' as mixins;
|
||||||
|
|
||||||
|
.clock-container {
|
||||||
|
height: var(--clock-size);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1 1 0px;
|
||||||
|
|
||||||
|
@media (max-width: 659px) {
|
||||||
|
height: 48px; // XS size
|
||||||
|
}
|
||||||
|
|
||||||
|
.clock-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
p {
|
||||||
|
text-wrap: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
width: auto;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { Meta, Canvas } from '@storybook/blocks';
|
||||||
|
|
||||||
|
import * as ClockWallStories from './ClockWall.stories.svelte';
|
||||||
|
|
||||||
|
<Meta of={ClockWallStories} />
|
||||||
|
|
||||||
|
# ClockWall
|
||||||
|
|
||||||
|
The `ClockWall` component displays a row of analog clocks for different cities and timezones. Use it paired with the overall headline of a graphics blog page to show the time of multiple cities involved in a breaking news event.
|
||||||
|
|
||||||
|
Use the [IANA tz database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) to find valid `tzIdentifier` strings.
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script>
|
||||||
|
import { ClockWall } from '@reuters-graphics/graphics-components';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ClockWall
|
||||||
|
cities={[
|
||||||
|
{ name: 'Tehran', tzIdentifier: 'Asia/Tehran' },
|
||||||
|
{ name: 'Tel Aviv', tzIdentifier: 'Asia/Tel_Aviv' },
|
||||||
|
{ name: 'Washington D.C.', tzIdentifier: 'America/New_York' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
<Canvas of={ClockWallStories.Demo} />
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script module lang="ts">
|
||||||
|
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||||
|
import ClockWall from './ClockWall.svelte';
|
||||||
|
|
||||||
|
const { Story } = defineMeta({
|
||||||
|
title: 'Components/Blog/ClockWall',
|
||||||
|
component: ClockWall,
|
||||||
|
argTypes: {
|
||||||
|
clockSize: {
|
||||||
|
control: 'select',
|
||||||
|
options: ['XS', 'MD', 'LG', 'XL'],
|
||||||
|
},
|
||||||
|
clockWeight: {
|
||||||
|
control: 'select',
|
||||||
|
options: ['Light', 'Normal', 'Bold'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Story name="Demo" />
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { ComponentProps } from 'svelte';
|
||||||
|
import type { ContainerWidth } from '../@types/global';
|
||||||
|
import Block from '../Block/Block.svelte';
|
||||||
|
import Clock from './Clock.svelte';
|
||||||
|
|
||||||
|
type ClockProps = ComponentProps<typeof Clock>;
|
||||||
|
|
||||||
|
interface City {
|
||||||
|
name: string;
|
||||||
|
tzIdentifier: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
cities?: City[];
|
||||||
|
width?: ContainerWidth;
|
||||||
|
clockSize?: ClockProps['clockSize'];
|
||||||
|
clockWeight?: ClockProps['clockWeight'];
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
cities = [
|
||||||
|
{ name: 'Tehran', tzIdentifier: 'Asia/Tehran' },
|
||||||
|
{ name: 'Tel Aviv', tzIdentifier: 'Asia/Tel_Aviv' },
|
||||||
|
{ name: 'Washington D.C.', tzIdentifier: 'America/New_York' },
|
||||||
|
],
|
||||||
|
width = 'normal',
|
||||||
|
clockSize = 'XS',
|
||||||
|
clockWeight = 'Bold',
|
||||||
|
}: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Block {width} class="my-6">
|
||||||
|
<div id="clock-group">
|
||||||
|
{#each cities as city (city.tzIdentifier)}
|
||||||
|
<Clock
|
||||||
|
name={city.name}
|
||||||
|
tzIdentifier={city.tzIdentifier}
|
||||||
|
{clockSize}
|
||||||
|
{clockWeight}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</Block>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '../../scss/mixins' as mixins;
|
||||||
|
|
||||||
|
div#clock-group {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0px auto;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px 1rem;
|
||||||
|
justify-content: space-around;
|
||||||
|
|
||||||
|
@media (max-width: 659px) {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Meta, Canvas } from '@storybook/blocks';
|
||||||
|
|
||||||
|
import * as KinesisLogoStories from './KinesisLogo.stories.svelte';
|
||||||
|
|
||||||
|
<Meta of={KinesisLogoStories} />
|
||||||
|
|
||||||
|
# KinesisLogo
|
||||||
|
|
||||||
|
The `KinesisLogo` component contains the official Kinesis logo.
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script>
|
||||||
|
import { KinesisLogo } from '@reuters-graphics/graphics-components';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<KinesisLogo />
|
||||||
|
```
|
||||||
|
|
||||||
|
<Canvas of={KinesisLogoStories.Demo} />
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<script module lang="ts">
|
||||||
|
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||||
|
import KinesisLogo from './KinesisLogo.svelte';
|
||||||
|
|
||||||
|
const { Story } = defineMeta({
|
||||||
|
title: 'Components/Logos/KinesisLogo',
|
||||||
|
component: KinesisLogo,
|
||||||
|
argTypes: {
|
||||||
|
colour: { control: 'color' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Story name="Demo" />
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
<!-- @component `ReutersLogo` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-logos-reuterslogo--docs) -->
|
||||||
|
<script lang="ts">
|
||||||
|
interface Props {
|
||||||
|
/** "Kinesis" colour */
|
||||||
|
colour?: string;
|
||||||
|
/** CSS width value */
|
||||||
|
width?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { colour = '#D64000', width = '100%' }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
viewBox="0 0 558.7 558.7"
|
||||||
|
style="width: {width}; --logoColour: {colour};"
|
||||||
|
>
|
||||||
|
<g id="Primary_Logo" data-name="Primary Logo">
|
||||||
|
<path
|
||||||
|
class="cls-1"
|
||||||
|
d="M483,75c7.1,7.1,11,16.6,11,26.7s-3.9,19.6-11,26.7c-14.7,14.7-38.6,14.7-53.3,0-7.1-7.1-11-16.6-11-26.7s3.9-19.6,11-26.7c7.4-7.4,17-11.1,26.7-11.1s19.3,3.7,26.7,11.1Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="cls-1"
|
||||||
|
d="M75.1,483.6c-14.7-14.7-14.7-38.7,0-53.4,7.1-7.1,16.6-11.1,26.7-11.1s19.6,3.9,26.7,11.1c14.7,14.7,14.7,38.7,0,53.4-7.1,7.1-16.6,11.1-26.7,11.1s-19.6-3.9-26.7-11.1Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="cls-1"
|
||||||
|
d="M362.6,77.9h-.3c-8-3.5-14.3-9.8-17.6-17.9-3.4-8.1-3.4-17.1,0-25.3,3.4-8.1,9.7-14.5,17.9-17.9,4.1-1.7,8.4-2.5,12.6-2.5s8.6.9,12.6,2.5c16.8,7,24.8,26.3,17.9,43.2-7,16.8-26.3,24.8-43.1,17.9h0Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="cls-1"
|
||||||
|
d="M183.1,478.3c4.3,0,8.5.9,12.6,2.5,16.8,7,24.8,26.3,17.9,43.2-3.4,8.2-9.7,14.5-17.9,17.9-8.1,3.4-17.1,3.4-25.3,0-16.8-7-24.8-26.3-17.9-43.2,3.4-8.1,9.7-14.5,17.9-17.9,4.1-1.7,8.4-2.5,12.6-2.5h0s0,0,0,0Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="cls-1"
|
||||||
|
d="M307.4,28.3c0,15.2-12,27.6-27,28.3h-1.3c-15.6,0-28.3-12.7-28.3-28.3S263.5,0,279.1,0s28.3,12.8,28.3,28.4h0s0,0,0,0Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="cls-1"
|
||||||
|
d="M250.8,530.4c0-15.6,12.7-28.3,28.3-28.3s28.3,12.7,28.3,28.3-12.7,28.3-28.3,28.3-28.3-12.7-28.3-28.3Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="cls-1"
|
||||||
|
d="M192.2,69.2c-5.8,2.4-12.2,2.4-18,0-5.9-2.4-10.4-7-12.8-12.8-5-12.1.7-25.9,12.8-30.9,2.9-1.2,6-1.8,9-1.8,9.3,0,18.1,5.5,21.8,14.6,2.4,5.9,2.4,12.3,0,18.1-2.4,5.9-6.9,10.4-12.8,12.8h0Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="cls-1"
|
||||||
|
d="M365.9,489.5c3-1.2,6-1.8,9.1-1.8s6.1.6,9,1.8c5.9,2.4,10.4,7,12.8,12.8,2.4,5.9,2.4,12.3,0,18.1-2.4,5.9-6.9,10.4-12.8,12.8-5.9,2.4-12.2,2.4-18.1,0-5.9-2.4-10.4-6.9-12.8-12.8-2.4-5.9-2.4-12.2,0-18.1,2.4-5.8,6.9-10.4,12.8-12.8h0Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="cls-1"
|
||||||
|
d="M115.1,88.6c7.2,7.3,7.4,18.9.4,26.4l-.4.4c-7.4,7.4-19.4,7.4-26.7,0-7.4-7.4-7.4-19.4,0-26.8,3.7-3.7,8.5-5.5,13.4-5.5s9.7,1.8,13.4,5.5h0,0Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="cls-1"
|
||||||
|
d="M442.8,443.5c3.6-3.7,8.5-5.5,13.3-5.5s9.7,1.8,13.4,5.5c3.6,3.5,5.5,8.3,5.5,13.4s-2,9.8-5.5,13.4c-7.4,7.4-19.3,7.4-26.7,0-3.6-3.6-5.5-8.4-5.5-13.4s2-9.8,5.5-13.4h0Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="cls-1"
|
||||||
|
d="M38.4,205.1c-12-5-17.8-18.8-12.8-30.9,2.4-5.9,6.9-10.4,12.8-12.8,3-1.2,6-1.8,9.1-1.8s6.1.6,9,1.8c12,5,17.7,18.8,12.8,30.9-2.4,5.9-6.9,10.4-12.8,12.8-5.9,2.4-12.3,2.4-18,0h0Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="cls-1"
|
||||||
|
d="M519.8,353.7c5.8,2.4,10.4,6.9,12.8,12.8,2.4,5.9,2.4,12.2,0,18.1-5,12-18.8,17.8-30.8,12.8-5.9-2.4-10.4-6.9-12.8-12.8-2.4-5.9-2.4-12.2,0-18.1,3.8-9.1,12.6-14.6,21.8-14.6s6,.6,9,1.8h0Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="cls-1"
|
||||||
|
d="M0,279.5c0-15.6,12.7-28.3,28.3-28.3s28.3,12.7,28.3,28.3-12.7,28.3-28.3,28.3S0,295.1,0,279.5Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="cls-1"
|
||||||
|
d="M501.6,279.5c0-15.6,12.7-28.3,28.3-28.3s28.3,12.7,28.3,28.3-12.7,28.3-28.3,28.3-28.3-12.7-28.3-28.3Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="cls-1"
|
||||||
|
d="M16.8,388.1c-7-16.9,1-36.2,17.9-43.2,8.1-3.4,17.1-3.4,25.3,0,8.1,3.4,14.5,9.7,17.9,17.9s3.4,17.1,0,25.3c-3.4,8.1-9.7,14.5-17.9,17.9s-17.1,3.4-25.3,0c-8.1-3.4-14.5-9.7-17.9-17.9Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="cls-1"
|
||||||
|
d="M480.1,195.9c-3.4-8.1-3.4-17.1,0-25.2,3.4-8.1,9.7-14.5,17.9-17.9,4.1-1.7,8.4-2.5,12.6-2.5,13,0,25.3,7.7,30.5,20.4,7,16.9-1,36.2-17.9,43.2-8.1,3.4-17.1,3.4-25.3,0-8.1-3.4-14.5-9.7-17.9-17.9h0,0Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="cls-1"
|
||||||
|
d="M359.3,359.7c-10.4,10.3-10.4,27.2,0,37.5,10.4,10.4,27.1,10.4,37.5,0,5-5,7.8-11.6,7.8-18.7s-2.8-13.8-7.8-18.7c-5.2-5.2-11.9-7.7-18.7-7.7s-13.6,2.6-18.8,7.7h0Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="cls-1"
|
||||||
|
d="M138.3,263.3c-11.2-3-17.8-14.5-14.8-25.7,1.5-5.4,4.9-9.9,9.8-12.8,3.2-1.9,6.8-2.8,10.4-2.8s3.6.2,5.4.7c5.4,1.5,9.9,4.9,12.7,9.8s3.5,10.5,2.1,16c-3,11.2-14.5,17.9-25.7,14.9h0,0Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="cls-1"
|
||||||
|
d="M408.9,335.9c-5.4-1.5-9.9-4.9-12.7-9.8s-3.5-10.5-2.1-16c1.5-5.4,4.9-10,9.8-12.8,3.2-1.9,6.9-2.8,10.5-2.8s3,.1,4.5.5l1,.3c11.2,3,17.8,14.6,14.8,25.7-1.5,5.4-4.9,10-9.8,12.8-4.9,2.8-10.5,3.6-16,2.1h0s0,0,0,0Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="cls-1"
|
||||||
|
d="M127.6,320c-2.3-8.7,2.5-17.5,10.9-20.4l1-.3c8.9-2.4,18.2,3,20.6,11.9,1.2,4.4.6,8.9-1.7,12.8-2.2,3.9-5.9,6.7-10.2,7.9-4.4,1.2-8.8.6-12.8-1.7-3.9-2.2-6.7-5.9-7.8-10.2h0s0,0,0,0Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="cls-1"
|
||||||
|
d="M405.9,257.8l-.7-.4c-3.5-2.3-6-5.7-7.1-9.8-2.4-9,2.9-18.2,11.9-20.6,8.9-2.4,18.2,2.9,20.6,11.9,2.4,9-2.9,18.2-11.9,20.6-4.3,1.2-8.8.5-12.8-1.7h0Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="cls-1"
|
||||||
|
d="M189.4,386.7l-.5.5c-4.9,4.9-13,4.9-17.9,0-4.9-4.9-4.9-12.9,0-17.9s13-4.9,17.9,0c2.4,2.4,3.7,5.6,3.7,9s-1.1,6.1-3.2,8.4Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="cls-1"
|
||||||
|
d="M386.9,171.2c2.4,2.4,3.7,5.6,3.7,9s-1.3,6.6-3.7,9c-4.9,4.9-13,4.9-17.9,0-2.4-2.4-3.7-5.6-3.7-9s1.3-6.6,3.7-9c2.4-2.4,5.6-3.7,9-3.7s6.6,1.3,9,3.7h0s0,0,0,0Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="cls-1"
|
||||||
|
d="M257.3,406.3c2.2,3.9,2.8,8.4,1.7,12.8-1.2,4.4-3.9,8-7.8,10.2s-8.4,2.8-12.8,1.7c-8.9-2.4-14.3-11.6-11.9-20.6,1.2-4.4,3.9-8,7.8-10.2,3.9-2.2,8.4-2.8,12.8-1.7,4.4,1.2,7.9,4,10.2,7.9h0s0,0,0,0Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="cls-1"
|
||||||
|
d="M300.6,152.5c-2.2-3.9-2.8-8.4-1.7-12.8,1.2-4.4,3.9-8,7.8-10.2,2.6-1.5,5.5-2.2,8.4-2.2s2.9.2,4.4.6c4.4,1.2,8,3.9,10.2,7.9,2.2,3.9,2.8,8.4,1.7,12.8-2.4,9-11.6,14.3-20.6,11.9-4.4-1.2-8-3.9-10.2-7.9h0Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="cls-1"
|
||||||
|
d="M325.9,396.6l.9.5c4.4,2.8,7.5,7.1,8.9,12.2,3,11.2-3.6,22.8-14.8,25.7-5.5,1.5-11.1.7-16-2.1-4.9-2.8-8.3-7.3-9.8-12.8-1.5-5.4-.7-11.1,2.1-16,2.8-4.9,7.3-8.3,12.7-9.8,1.8-.5,3.7-.7,5.5-.7,3.6,0,7.2,1,10.5,2.8h0Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="cls-1"
|
||||||
|
d="M248.2,164.3c-11.2,3-22.7-3.7-25.7-14.9-3-11.2,3.6-22.8,14.8-25.7,1.8-.5,3.6-.7,5.4-.7,9.3,0,17.8,6.2,20.3,15.6,3,11.2-3.6,22.8-14.8,25.7Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="cls-1"
|
||||||
|
d="M180,206.5c14.6,0,26.4-11.9,26.4-26.5s-11.8-26.5-26.4-26.5-26.4,11.9-26.4,26.5,11.8,26.5,26.4,26.5Z"
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: var(--logoColour);
|
||||||
|
stroke-width: 0px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -53,13 +53,16 @@ Disable interaction for static maps:
|
||||||
|
|
||||||
Use the `TileMapLayer` component to add GeoJSON data to your map. You can pass GeoJSON data directly or fetch it from a URL. Layer rendering order will directly correspond to the order in which you add the layers in the code.
|
Use the `TileMapLayer` component to add GeoJSON data to your map. You can pass GeoJSON data directly or fetch it from a URL. Layer rendering order will directly correspond to the order in which you add the layers in the code.
|
||||||
|
|
||||||
|
> **Note for TypeScript users:** When passing GeoJSON data objects, you'll need to type cast them using `as GeoJSON` to ensure TypeScript recognizes the correct type. This provides better type safety and error messages. See examples below.
|
||||||
|
|
||||||
<Canvas of={TileMapStories.WithGeoJSONLayers} />
|
<Canvas of={TileMapStories.WithGeoJSONLayers} />
|
||||||
|
|
||||||
### Basic example with local data
|
### Basic example with local data
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { TileMap, TileMapLayer } from '@reuters-graphics/graphics-components';
|
import { TileMap, TileMapLayer } from '@reuters-graphics/graphics-components';
|
||||||
|
import type { GeoJSON } from 'geojson';
|
||||||
|
|
||||||
const parkData = {
|
const parkData = {
|
||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection',
|
||||||
|
|
@ -88,7 +91,7 @@ Use the `TileMapLayer` component to add GeoJSON data to your map. You can pass G
|
||||||
<!-- Polygon fill -->
|
<!-- Polygon fill -->
|
||||||
<TileMapLayer
|
<TileMapLayer
|
||||||
id="park-fill"
|
id="park-fill"
|
||||||
data={parkData}
|
data={parkData as GeoJSON}
|
||||||
type="fill"
|
type="fill"
|
||||||
paint={{
|
paint={{
|
||||||
'fill-color': '#179639',
|
'fill-color': '#179639',
|
||||||
|
|
@ -99,7 +102,7 @@ Use the `TileMapLayer` component to add GeoJSON data to your map. You can pass G
|
||||||
<!-- Polygon outline -->
|
<!-- Polygon outline -->
|
||||||
<TileMapLayer
|
<TileMapLayer
|
||||||
id="park-outline"
|
id="park-outline"
|
||||||
data={parkData}
|
data={parkData as GeoJSON}
|
||||||
type="line"
|
type="line"
|
||||||
paint={{
|
paint={{
|
||||||
'line-color': '#228b22',
|
'line-color': '#228b22',
|
||||||
|
|
@ -130,7 +133,9 @@ You can also pass a URL string to fetch GeoJSON data:
|
||||||
### Adding point markers
|
### Adding point markers
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script>
|
<script lang="ts">
|
||||||
|
import type { GeoJSON } from 'geojson';
|
||||||
|
|
||||||
const officeLocation = {
|
const officeLocation = {
|
||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection',
|
||||||
features: [
|
features: [
|
||||||
|
|
@ -150,7 +155,7 @@ You can also pass a URL string to fetch GeoJSON data:
|
||||||
<!-- Point marker -->
|
<!-- Point marker -->
|
||||||
<TileMapLayer
|
<TileMapLayer
|
||||||
id="office-point"
|
id="office-point"
|
||||||
data={officeLocation}
|
data={officeLocation as GeoJSON}
|
||||||
type="circle"
|
type="circle"
|
||||||
paint={{
|
paint={{
|
||||||
'circle-radius': 6,
|
'circle-radius': 6,
|
||||||
|
|
@ -163,7 +168,7 @@ You can also pass a URL string to fetch GeoJSON data:
|
||||||
<!-- Text label -->
|
<!-- Text label -->
|
||||||
<TileMapLayer
|
<TileMapLayer
|
||||||
id="office-label"
|
id="office-label"
|
||||||
data={officeLocation}
|
data={officeLocation as GeoJSON}
|
||||||
type="symbol"
|
type="symbol"
|
||||||
layout={{
|
layout={{
|
||||||
'text-field': 'Office',
|
'text-field': 'Office',
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,12 @@ export {
|
||||||
registerPageview,
|
registerPageview,
|
||||||
} from './components/Analytics/Analytics.svelte';
|
} from './components/Analytics/Analytics.svelte';
|
||||||
export { default as Article } from './components/Article/Article.svelte';
|
export { default as Article } from './components/Article/Article.svelte';
|
||||||
|
export { default as BlogPost } from './components/BlogPost/BlogPost.svelte';
|
||||||
|
export { default as BlogTOC } from './components/BlogTOC/BlogTOC.svelte';
|
||||||
export { default as AdScripts } from './components/AdSlot/AdScripts.svelte';
|
export { default as AdScripts } from './components/AdSlot/AdScripts.svelte';
|
||||||
export { default as BeforeAfter } from './components/BeforeAfter/BeforeAfter.svelte';
|
export { default as BeforeAfter } from './components/BeforeAfter/BeforeAfter.svelte';
|
||||||
export { default as Block } from './components/Block/Block.svelte';
|
export { default as Block } from './components/Block/Block.svelte';
|
||||||
|
export { default as ClockWall } from './components/ClockWall/ClockWall.svelte';
|
||||||
export { default as BodyText } from './components/BodyText/BodyText.svelte';
|
export { default as BodyText } from './components/BodyText/BodyText.svelte';
|
||||||
export { default as Byline } from './components/Byline/Byline.svelte';
|
export { default as Byline } from './components/Byline/Byline.svelte';
|
||||||
export { default as DatawrapperChart } from './components/DatawrapperChart/DatawrapperChart.svelte';
|
export { default as DatawrapperChart } from './components/DatawrapperChart/DatawrapperChart.svelte';
|
||||||
|
|
@ -29,6 +32,7 @@ export { default as HorizontalScroller } from './components/HorizontalScroller/H
|
||||||
export { default as EndNotes } from './components/EndNotes/EndNotes.svelte';
|
export { default as EndNotes } from './components/EndNotes/EndNotes.svelte';
|
||||||
export { default as InfoBox } from './components/InfoBox/InfoBox.svelte';
|
export { default as InfoBox } from './components/InfoBox/InfoBox.svelte';
|
||||||
export { default as InlineAd } from './components/AdSlot/InlineAd.svelte';
|
export { default as InlineAd } from './components/AdSlot/InlineAd.svelte';
|
||||||
|
export { default as KinesisLogo } from './components/KinesisLogo/KinesisLogo.svelte';
|
||||||
export { default as LeaderboardAd } from './components/AdSlot/LeaderboardAd.svelte';
|
export { default as LeaderboardAd } from './components/AdSlot/LeaderboardAd.svelte';
|
||||||
export { default as TileMap } from './components/TileMap/TileMap.svelte';
|
export { default as TileMap } from './components/TileMap/TileMap.svelte';
|
||||||
export { default as TileMapLayer } from './components/TileMap/TileMapLayer.svelte';
|
export { default as TileMapLayer } from './components/TileMap/TileMapLayer.svelte';
|
||||||
|
|
|
||||||
|
|
@ -16,4 +16,13 @@ declare module 'journalize' {
|
||||||
* @returns The converted value
|
* @returns The converted value
|
||||||
*/
|
*/
|
||||||
export function intcomma(val: number | string): string;
|
export function intcomma(val: number | string): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an AP-formatted month string that corresponds with the supplied
|
||||||
|
* Date. If an `input` is not passed, it will use the result of `new Date();`.
|
||||||
|
*
|
||||||
|
* @param date - The supplied Date
|
||||||
|
* @returns The converted date as a string
|
||||||
|
*/
|
||||||
|
export function apmonth(date?: Date): string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import slugify from 'slugify';
|
import slug from 'slugify';
|
||||||
|
|
||||||
/** Helper function to generate a random 4-character string */
|
/** Helper function to generate a random 4-character string */
|
||||||
export const random4 = () =>
|
export const random4 = () =>
|
||||||
|
|
@ -10,7 +10,7 @@ export const random4 = () =>
|
||||||
* Custom function that returns an author page URL.
|
* Custom function that returns an author page URL.
|
||||||
*/
|
*/
|
||||||
export const getAuthorPageUrl = (author: string): string => {
|
export const getAuthorPageUrl = (author: string): string => {
|
||||||
const authorSlug = slugify(author.trim(), { lower: true });
|
const authorSlug = slug(author.trim(), { lower: true });
|
||||||
return `https://www.reuters.com/authors/${authorSlug}/`;
|
return `https://www.reuters.com/authors/${authorSlug}/`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -93,3 +93,12 @@ const prettifyAmPm = (text: string) => {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a string into a URL-friendly slug.
|
||||||
|
*
|
||||||
|
* @param str The string to be slugified.
|
||||||
|
* @returns The slugified string.
|
||||||
|
*/
|
||||||
|
export const slugify = (str: string) =>
|
||||||
|
slug(str, { lower: true, strict: true });
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue