Integrations

This commit is contained in:
wires 2026-05-11 22:00:11 -04:00
parent 024857de84
commit d8cc0cb3ae
361 changed files with 17410 additions and 51 deletions

View file

@ -0,0 +1,647 @@
# ArchieML-Veronica Language Reference
Veronica is a more specific dialect of [ArchieML](https://archieml.org) designed for newsrooms and content editors. While ArchieML is "forgiving," Veronica makes some syntax more explicit to prevent common parsing errors, especially when working with multiline content and nested structures.
## Table of Contents
- [Keys and Values](#keys-and-values)
- [Objects](#objects)
- [Nested Objects](#nested-objects)
- [Named Closures](#named-closures-veronica-extension)
- [Arrays](#arrays)
- [Arrays of Objects](#arrays-of-objects)
- [Arrays of Strings (Simple Arrays)](#arrays-of-strings-simple-arrays)
- [Freeform Arrays](#freeform-arrays)
- [Nested Arrays](#nested-arrays)
- [Named Array Closures](#named-array-closures-veronica-extension)
- [Repeating Keys in Arrays](#repeating-keys-in-arrays-veronica-extension)
- [Multiline Values](#multiline-values)
- [Veronica Multiline Syntax](#veronica-multiline-syntax-veronica-extension)
- [ArchieML Multiline Syntax](#archieml-multiline-syntax)
- [Comments and Ignored Content](#comments-and-ignored-content)
- [Escaping](#escaping)
- [Hooks and Customization](#hooks-and-customization)
## Keys and Values
The simplest form of ArchieML is a key-value pair. Keys are defined by a colon, with the key on the left and the value on the right.
```
title: My Article
author: Jane Smith
published: 2024-01-15
```
**Result:**
```json
{
"title": "My Article",
"author": "Jane Smith",
"published": "2024-01-15"
}
```
### Key Rules
- Keys can contain letters, numbers, underscores, hyphens, and Unicode characters
- Keys **cannot** contain: whitespace, `{`, `}`, `[`, `]`, `:`, `.`, or `+`
- Keys are case-sensitive (`Title` and `title` are different)
- Whitespace around keys and values is automatically trimmed
### Ignored Content
Any text that doesn't match ArchieML syntax is ignored, allowing you to include notes and comments freely:
```
This is just a note that will be ignored.
title: My Article
Here's another note about the article.
author: Jane Smith
```
## Objects
### Dot Notation
You can create nested objects using dot notation:
```
colors.red: #ff0000
colors.green: #00ff00
colors.blue: #0000ff
```
**Result:**
```json
{
"colors": {
"red": "#ff0000",
"green": "#00ff00",
"blue": "#0000ff"
}
}
```
### Object Blocks
For more complex objects, use curly braces:
```
{colors}
red: #ff0000
green: #00ff00
blue: #0000ff
{}
```
**Result:** Same as above.
### Nested Objects
Prepend a period (`.`) to a block name to nest it within the current object:
```
{author}
name: Jane Smith
email: jane@example.com
{.social}
twitter: @janesmith
github: janesmith
{}
bio: Award-winning journalist
{}
```
**Result:**
```json
{
"author": {
"name": "Jane Smith",
"email": "jane@example.com",
"social": {
"twitter": "@janesmith",
"github": "janesmith"
},
"bio": "Award-winning journalist"
}
}
```
### Closing Objects
You can close an object in several ways:
1. **Empty braces** `{}` - closes the current object
2. **Opening a new object** at the same level
3. **Named closure** (Veronica extension) - see below
### Named Closures (Veronica Extension)
Veronica extends ArchieML with named closures, allowing you to close a specific object by name, even if you're nested several levels deep:
```
{outer}
value: test
{.middle}
value: nested
{.inner}
value: deep
{/middle}
This closes middle and inner, returning to outer scope.
stillOuter: yes
{}
```
**Result:**
```json
{
"outer": {
"value": "test",
"middle": {
"value": "nested",
"inner": {
"value": "deep"
}
},
"stillOuter": "yes"
}
}
```
**Important:** The slash must be flush with the opening brace: `{/name}` works, but `{ /name }` does not.
## Arrays
### Arrays of Objects
Arrays are defined using square brackets. The first key that repeats signals the start of a new item:
```
[people]
name: Alice
age: 30
name: Bob
age: 25
[]
```
**Result:**
```json
{
"people": [
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 25}
]
}
```
### Arrays of Strings (Simple Arrays)
For simple lists of strings, use asterisks:
```
[tags]
* news
* technology
* AI
[]
```
**Result:**
```json
{
"tags": ["news", "technology", "AI"]
}
```
### Freeform Arrays
Freeform arrays (marked with `[+arrayName]`) preserve the order of different types of content. They're useful for mixed content like articles with text, images, and pull quotes:
```
[+content]
This is a paragraph of text.
Another paragraph here.
{.image}
src: photo.jpg
caption: A beautiful photo
{}
More text after the image.
{.quote}
text: An inspiring quotation
author: Famous Person
{}
[]
```
**Result:**
```json
{
"content": [
{"type": "text", "value": "This is a paragraph of text."},
{"type": "text", "value": "Another paragraph here."},
{"type": "image", "value": {"src": "photo.jpg", "caption": "A beautiful photo"}},
{"type": "text", "value": "More text after the image."},
{"type": "quote", "value": {"text": "An inspiring quotation", "author": "Famous Person"}}
]
}
```
### Nested Arrays
Prepend a period to an array name to nest it:
```
[sections]
title: Introduction
[.subsections]
heading: Background
content: Context here
[]
heading: Methodology
content: How we did it
[]
[/sections]
```
**Result:**
```json
{
"sections": [
{
"title": "Introduction",
"subsections": [
{"heading": "Background", "content": "Context here"},
{"heading": "Methodology", "content": "How we did it"}
]
}
]
}
```
### Named Array Closures (Veronica Extension)
Like objects, arrays support named closures to jump out of nested structures:
```
[parent]
value: test
[.nested]
* one
* two
[/parent]
This is outside the parent array.
```
This is especially useful when you have deeply nested arrays and want to exit multiple levels at once.
### Repeating Keys in Arrays (Veronica Extension)
**This is a key difference from standard ArchieML.**
In Veronica, arrays start a new item when **any** key is redefined, not just the first key:
```
[items]
name: First
description: First item
color: red
description: Second item
name: Second
color: blue
[]
```
**Result:**
```json
{
"items": [
{"name": "First", "description": "First item", "color": "red"},
{"name": "Second", "description": "Second item", "color": "blue"}
]
}
```
In standard ArchieML, the second item would need to redefine `name` (the first key) to start a new item. Veronica is more flexible: redefining `description` also triggers a new item.
## Multiline Values
### Veronica Multiline Syntax (Veronica Extension)
Veronica introduces an explicit multiline syntax that's less ambiguous than ArchieML's `:end` syntax:
```
description::
This is the first line of my description.
This is the second paragraph.
This can contain [brackets] and {braces} safely.
::description
```
**Result:**
```json
{
"description": "This is the first line of my description.\n\nThis is the second paragraph.\n\nThis can contain [brackets] and {braces} safely."
}
```
**Syntax:**
- Open with `key::` (key followed by double colon)
- Write your content on following lines
- Close with `::key` (double colon followed by the same key name)
This syntax is more explicit and helps prevent accidentally consuming subsequent content.
### ArchieML Multiline Syntax
Veronica still supports the traditional ArchieML multiline syntax with `:end`:
```
description:
This is a multiline value.
It continues until :end is found.
:end
```
**Important:** Within multiline blocks, ArchieML syntax is **not** parsed. To include literal backslashes or the `:end` marker, use a backslash escape:
```
code:
To end a multiline, use \:end
A literal backslash: \\
:end
```
## Comments and Ignored Content
### Block Comments
Use `:skip` and `:endskip` to comment out entire sections:
```
title: My Article
:skip
This entire section is ignored.
author: Will Not Parse
{test}
{}
:endskip
published: 2024-01-15
```
**Result:**
```json
{
"title": "My Article",
"published": "2024-01-15"
}
```
### Stop Parsing
Use `:ignore` to immediately stop parsing. Everything after `:ignore` is ignored:
```
title: My Article
author: Jane Smith
:ignore
This and everything below is completely ignored.
Nothing here will be parsed.
```
**Result:**
```json
{
"title": "My Article",
"author": "Jane Smith"
}
```
## Escaping
Use a backslash (`\`) to escape special ArchieML characters when they appear at the start of a line in a multiline context:
```
description:
\[This is not an array]
\{This is not an object}
\:end is not the end marker
The real end:
:end
```
**Note:** Escaping only works in multiline contexts and only for characters at the start of a line. Outside of multiline blocks, special characters are generally ignored if they don't form valid syntax.
## Hooks and Customization
Veronica provides several hooks for customizing parsing behavior:
### `onFieldName(name: string) => string`
Transform field names during parsing. Useful for normalizing keys:
```javascript
const result = parse(text, {
onFieldName: (name) => name.toLowerCase()
});
```
This is particularly helpful when working with Google Docs, which may capitalize the first word of a line.
### `onValue(value: any, key: string) => any`
Transform values during parsing. Useful for type coercion:
```javascript
const result = parse(text, {
onValue: (value, key) => {
// Auto-convert boolean strings
if (value === "true") return true;
if (value === "false") return false;
// Auto-convert numbers
if (/^\d+$/.test(value)) return parseInt(value);
if (/^\d+\.\d+$/.test(value)) return parseFloat(value);
// Auto-parse ISO dates
if (/^\d{4}-\d{2}-\d{2}/.test(value)) {
return new Date(value);
}
return value;
}
});
```
### `onEnter(keypath: string[], item: any) => void`
Called when entering an object or array. The `keypath` is an array of strings representing the path from the root:
```javascript
const result = parse(text, {
onEnter: (keypath, item) => {
console.log(`Entering: ${keypath.join('.')}`);
console.log(`Type: ${Array.isArray(item) ? 'array' : 'object'}`);
}
});
```
### `onExit(keypath: string[], item: any) => any`
Called when exiting an object or array. This is ideal for validation and adding computed properties. If you return a value, it replaces the item in the output:
```javascript
const result = parse(text, {
onExit: (keypath, item) => {
// Validate required fields
if (keypath[0] === "person" && !item.name) {
throw new Error("Person must have a name");
}
// Add computed properties
if (keypath[0] === "person" && item.firstName && item.lastName) {
item.fullName = `${item.firstName} ${item.lastName}`;
}
return item; // Return the modified item
}
});
```
**Example with validation:**
```javascript
const text = `
{author}
firstName: Jane
lastName: Smith
{/author}
`;
const result = parse(text, {
onExit: (keypath, item) => {
if (keypath[0] === "author") {
if (!item.firstName || !item.lastName) {
throw new Error("Author must have both firstName and lastName");
}
// Add computed fullName
item.fullName = `${item.firstName} ${item.lastName}`;
}
return item;
}
});
// Result: { author: { firstName: "Jane", lastName: "Smith", fullName: "Jane Smith" } }
```
### `verbose: boolean`
Enable verbose logging to see detailed parsing information:
```javascript
const result = parse(text, {
verbose: true
});
```
## Complete Example
Here's a comprehensive example showing many Veronica features:
```
title: Understanding Veronica
subtitle: A Guide to Structured Content
published: 2024-01-15
{metadata}
tags.primary: archieml
tags.secondary: parsing
wordCount: 1500
{/metadata}
intro::
This is a multiline introduction to Veronica.
It supports multiple paragraphs and preserves formatting.
::intro
[sections]
heading: Introduction
body: Welcome to Veronica, a dialect of ArchieML.
heading: Features
body: Veronica adds several improvements over standard ArchieML.
[.examples]
* Named closures
* Explicit multiline syntax
* Better array handling
[/sections]
[+mixedContent]
This is a text paragraph.
{.callout}
type: warning
message: Remember to close your arrays!
{}
Another text paragraph here.
[]
{author}
firstName: Jane
lastName: Smith
email: jane@example.com
{.social}
twitter: @janesmith
github: janesmith
{/author}
footer: Copyright 2024
```
This example demonstrates:
- Simple key-value pairs
- Object blocks with nested objects
- Dot notation for nested keys
- Veronica multiline syntax (`key::` / `::key`)
- Arrays of objects with nested arrays
- Freeform arrays with mixed content
- Named closures (`{/author}`, `[/sections]`)
## Summary of Veronica Extensions
Veronica extends ArchieML with these key features:
1. **Named closures** - `{/name}` and `[/name]` to exit specific nesting levels
2. **Explicit multiline syntax** - `key::` / `::key` delimiters for unambiguous multiline values
3. **Flexible array items** - Any redefined key starts a new array item, not just the first key
4. **Lifecycle hooks** - `onEnter` and `onExit` callbacks for validation and transformation
5. **Value transformation** - `onFieldName` and `onValue` hooks for custom processing
These changes make Veronica more predictable and robust when working with complex structured content, especially in collaborative editing environments like Google Docs.

View file

@ -0,0 +1,483 @@
---
applyTo: "**/*.svelte"
---
## Graphics components
The `@reuters-graphics/graphics-components` library includes pre-styled components for easily adding graphics or other elements to a page.
### Adding new components to a page
Components from the `@reuters-graphics/graphics-components` library are often added to the `#each` loop in `src/lib/App.svelte` that loops over `content.blocks`.
`content` represents text content pulled from our CMS as JSON that is passed into components via props.
Each block in `content.blocks` (i.e., "content block") is usually an object with a `type` property and additional properties specific to the block type. For example:
- **Text Block**:
```json
{
"type": "text",
"text": "This is a text block."
}
```
- **AI Graphic Block**
```json
{
"type": "ai-graphic",
"chart": "AiMap",
"width": "normal",
"textWidth": "normal",
"title": "Optional title of the graphic",
"description": "Optional chatter describes more about the graphic.",
"notes": "Note: Optional note clarifying something in the data.\r\n\r\nSource: Optional source of the data.",
"altText": "Add a description of the graphic for screen readers. This is invisible on the page."
}
```
To add a new component to the loop:
1. Import the component in the script portion of the Svelte component.
2. Add a new `else if` condition in the `{#each content.blocks as block}` loop to handle the new block type.
3. Pass the required props to the component, ensuring they match the structure of the content block object.
For example, to add a new FeaturePhoto:
```svelte
<script lang="ts">
// ...
import { assets } from '$app/paths';
import {
Article,
Analytics,
BodyText,
EndNotes,
SiteHeadline,
GraphicBlock,
InlineAd,
FeautePhoto, // Add the component to others already imported
} from '@reuters-graphics/graphics-components';
// ...
</script>
{#each content.blocks as block}
<!-- Text block -->
{#if block.type === 'text'}
<BodyText text={block.text} />
<!-- Other block types -->
<!-- Add new FeaturePhoto block -->
{:else if block.type === 'feature-photo'}
<FeaturePhoto
src="{assets}/{block.src}"
alt={block.alt}
caption={block.caption}
credit={block.credit}
/>
{:else}
<LogBlock message={`Unknown block type: "${block.type}"`} />
{/if}
{/each}
```
### Paths to multimedia files
Notice in the example above, we append the `assets` variable from SvelteKit's `$app/paths` module to the `src` path we got from the content block.
Always assume that paths to local multimedia files including images and videos specified in content blocks are relative and must be prefixed with the `assets` variable to make them absolute. For example:
```svelte
<FeaturePhoto
src="{assets}/{block.src}"
alt={block.alt}
caption={block.caption}
credit={block.credit}
/>
```
### Adding AI Graphics
AI (i.e., Adobe Illustrator) graphics are added to the page by importing a component from the `src/lib/ai2svelte/` directory and then adding that graphic to the `aiCharts` object in `src/lib/App.svelte`. Then the object key for that chart is included in the content block's `chart` property.
For example, to use an AI graphic from `src/lib/ai2svelte/map.svelte`:
- The AI graphic component is imported and added to the `aiCharts` object in `src/lib/App.svelte`:
```svelte
<script lang="ts">
// ...
import Map from './ai2svelte/map.svelte';
// ...
const aiCharts = {
// Other AI graphics ...
Map, // Added to the object
};
</script>
```
- Now the content block will specify the key to the chart in `aiCharts` in the `chart` property:
```json
{
"type": "ai-graphic",
"chart": "Map",
"width": "normal",
"textWidth": "normal",
"title": "My map",
"description": "A map of the area",
"notes": "Source: DataSource.org",
"altText": "A map of a specific area showing something interesting"
}
```
- Now the `{:else if block.type === 'ai-graphic'}` block in `src/lib/App.svelte` uses that key to get the component:
```svelte
{#each content.blocks as block}
<!-- Text block -->
{#if block.type === 'text'}
<BodyText text={block.text} />
<!-- Ai2svelte graphic block -->
{:else if block.type === 'ai-graphic'}
{#if !aiCharts[block.chart]}
<LogBlock message={`Unable to find "${block.chart}" in aiCharts`} />
{:else}
{@const AiChart = aiCharts[block.chart]}
<GraphicBlock
id={block.chart}
width={containerWidth(block.width)}
title={block.title}
description={block.description}
notes={block.notes}
ariaDescription={block.altText}
>
<AiChart assetsPath={assets || '/'} />
</GraphicBlock>
{/if}
<!-- Other block types -->
{/if}
{/each}
```
### Refer to graphics components Storybook
If you're unsure how to implement a particular graphics component, suggest the user check the Storybook documentation site, which is hosted on GitHub at https://reuters-graphics.github.io/graphics-components/.
### Writing content blocks in our CMS
While the text content in the `content` object is formatted as JSON, that data is written into our CMS (which is called "RNGS.io") using ArchieML syntax.
**When suggesting what content blocks to add for components, please also suggest how to write that content in our CMS (RNGS.io) using ArchieML.**
#### ArchieML
ArchieML is a lightweight and intuitive markup language that allows for easy structuring of data within text documents. It is designed to be human-readable, very flexible, and is particularly useful for creating structured data by users who may never have seen ArchieML or any other markup language before.
##### Basic Syntax
- **Keys and Values**
- Definition: Key-value pairs are defined by a line starting with a key followed by a colon. Keys can include any unicode character except whitespace and specific characters used within ArchieML ({ } [ ] : . +).
- Example:
```
key: This is a value
☃: Unicode Snowman for you and you and you!
```
- Parsed JSON:
```json
{
"key": "This is a value",
"☃": "Unicode Snowman for you and you and you!"
}
```
- Whitespace around keys and values is ignored. Keys are case-sensitive.
- **Multi-line Values**: Multi-line values are anchored with `:end`. All whitespace is preserved.
- Example:
```
key: value
More value
Even more value
:end
```
- Parsed JSON:
```json
{
"key": "value\n More value\n\nEven more value"
}
```
- Escape Characters: Lines that would be interpreted as keys or commands can be escaped with a backslash `\`.
- Example:
```
key: value
\:end
:end
```
- Parsed JSON:
```json
{
"key": "value\n:end"
}
```
- **Nested Structures**
- **Dot-Notation**: Use dot-notation for creating nested objects.
- Example:
```
colors.red: #f00
colors.green: #0f0
colors.blue: #00f
```
- Parsed JSON:
```json
{
"colors": {
"red": "#f00",
"green": "#0f0",
"blue": "#00f"
}
}
```
- **Object Blocks**: Group keys using object blocks defined by {}. Close an object with {} or by starting a new object.
- Example:
```
{colors}
red: #f00
green: #0f0
blue: #00f
{}
{numbers}
one: 1
ten: 10
one-hundred: 100
{}
```
- Parsed JSON:
```json
{
"colors": {
"red": "#f00",
"green": "#0f0",
"blue": "#00f"
},
"numbers": {
"one": "1",
"ten": "10",
"one-hundred": "100"
}
}
```
- **Arrays**
- **Arrays of Objects**: Define arrays with brackets [arrayName]. New objects start when the first key is re-encountered.
- Example:
```
[arrayName]
name: Amanda
age: 26
name: Tessa
age: 30
[]
```
- Parsed JSON:
```json
{
"arrayName": [
{
"name": "Amanda",
"age": "26"
},
{
"name": "Tessa",
"age": "30"
}
]
}
```
- **Arrays of Strings**: Simple arrays use _ for elements. If _ is first, the array ignores key-value pairs.
- Example:
```
[days]
* Sunday
* Monday
* Tuesday
* Wednesday
* Thursday
* Friday
* Saturday
[]
```
- Parsed JSON:
```json
{
"days": [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
]
}
```
- **Nested Arrays**: Nested arrays use dot notation and are closed with `[]`.
- Example:
```
[days]
name: Monday
[.tasks]
* Clean dishes
* Pick up room
[]
name: Tuesday
[.tasks]
* Buy milk
[]
```
- Parsed JSON:
```json
{
"days": [
{
"name": "Monday",
"tasks": ["Clean dishes", "Pick up room"]
},
{
"name": "Tuesday",
"tasks": ["Buy milk"]
}
]
}
```
#### ArchieML conventions in our CMS
Usually, content blocks are written in our CMS as objects inside an ArchieML array called `blocks` and always start with a `type:` key/value pair that defines the type of the content block object.
For example, a user adding a content block for a FeaturePhoto component, might add the following to the existing `[blocks]` array in our CMS:
```
[blocks]
type: feature-photo
src: images/myPhoto.jpg
alt: Alt text for my photo...
... which extends over multiple lines.
:end
caption: A photo of something interesting.
credit: Jane Doe
[]
```
### Graphics components style tokens
As well as Svelte components, the graphics components library includes a tailwind-like style system that can be used to style components and other page elements by adding a class or through SCSS mixins.
These classes and mixins are defined as individual "tokens" representing the value for an individual style rule, like `font-size` or `color`. Each token sets just one style rule, and multiple tokens are combined together to style an element, like a `<div>`.
Each set of tokens has several levels that represent the different values a style rule can take in our design system and are grouped in how they're named to make them easier to remember.
For example, font weight tokens include `font-thin`, `font-light`, and `font-bold` which correspond to the style rules `font-weight: 100;`, `font-weight: 300;`, and `font-weight: 700;`, respectively. And those tokens can be applied via class name or SCSS mixin. For example:
```svelte
<!-- Bold text applied via class -->
<p class="font-bold">Here is some bold text with some <span>thin text</span> in it!</p>
<style lang="scss">
@use '@reuters-graphics/graphics-components/scss/mixins' as mixins;
// Thin text applied via SCSS mixin
span {
@include mixins.font-thin;
}
</style>
```
Not all our style tokens have both class names as well as SCSS mixins available to apply them.
**Please use the tokens defined in the SCSS partials in the [@reuters-graphics/graphics-components/dist/scss/tokens/ directory](./../../../node_modules/@reuters-graphics/graphics-components/dist/scss/tokens/) liberally in instructions and code samples, BUT be sure the token exists before suggesting it. DO NOT MAKE UP TOKENS, CLASS NAMES OR SCSS MIXINS!**
If you're not sure if there is a token to apply a particular style, you can refer the user to our Storybook documentation site for them at: https://reuters-graphics.github.io/graphics-components/?path=/docs/styles-intro--docs.
#### Using style tokens
To use a token to style an element, you can apply it directly to the element through a class name. For example, to apply the `text-primary` token (controlling font colour) you can apply it like this:
```svelte
<p class="text-primary">Lorem ipsum...</p>
```
OR you can apply some tokens via an SCSS mixin. For example:
```svelte
<p>Lorem ipsum...</p>
<style lang="scss">
@use '@reuters-graphics/graphics-components/scss/mixins' as mixins;
p {
@include mixins.text-primary;
}
</style>
```
**Be sure to always include the `@use` line that imports the SCSS mixins from the library in your SCSS/styling suggestions.**
Please consider the SCSS mixins and classes defined in the [@reuters-graphics/graphics-components/dist/scss/tokens/ directory](./../../../node_modules/@reuters-graphics/graphics-components/dist/scss/tokens/).
#### Spacing tokens
We have a special set of tokens to control spacing, i.e., paddings and margins. They operate like tailwind's padding and margin system. For example, `mt-1` represents `margin-top: 0.25rem;` and `px-2` represents `padding-right: 0.5rem; padding-left: 0.5rem;`, etc. These tokens can be applied only through a class.
These tokens are all defined as a combination of a prefix and a level. The prefix is something like `mb` for bottom margin or `py` for padding top and bottom. The level is a number representing how large the padding are margin should be. The levels go like this: 0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 16, then increasing by 4 each time up to 96. For example, the full token set for top margin would be `mt-0`, `mt-0.5`, `mt-1`, `mt-1.5`, and so on.
We also have a set of spacing tokens designed to work with _fluid_ typography. These are prefixed beginning with the letter `f`, for example, `fmb-1` represents a _fluid_ margin bottom, `margin-bottom: clamp(0.31rem, 0.31rem + 0vw, 0.31rem);`. These tokens can be applied through a class AND an SCSS mixin. For example:
```svelte
<p class="fmy-3">Some text with margin and padding</p>
<style lang="scss">
@use '@reuters-graphics/graphics-components/scss/mixins' as mixins;
p {
@include mixins.fpx-1;
}
</style>
```
You should recommend fluid margin and padding tokens for spacing fluidly-sized typographical elements or elements that are spaced _next to_ fluidly-sized typographical elements. Typographical elements include page headings, paragraphs or elements containing text, generally.

View file

@ -0,0 +1,66 @@
---
name: svelte-code-writer
description: CLI tools for Svelte 5 documentation lookup and code analysis. MUST be used whenever creating, editing or analyzing any Svelte component (.svelte) or Svelte module (.svelte.ts/.svelte.js). If possible, this skill should be executed within the svelte-file-editor agent for optimal results.
---
# Svelte 5 Code Writer
## CLI Tools
You have access to `@sveltejs/mcp` CLI for Svelte-specific assistance. Use these commands via `npx`:
### List Documentation Sections
```bash
npx @sveltejs/mcp list-sections
```
Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths.
### Get Documentation
```bash
npx @sveltejs/mcp get-documentation "<section1>,<section2>,..."
```
Retrieves full documentation for specified sections. Use after `list-sections` to fetch relevant docs.
**Example:**
```bash
npx @sveltejs/mcp get-documentation "$state,$derived,$effect"
```
### Svelte Autofixer
```bash
npx @sveltejs/mcp svelte-autofixer "<code_or_path>" [options]
```
Analyzes Svelte code and suggests fixes for common issues.
**Options:**
- `--async` - Enable async Svelte mode (default: false)
- `--svelte-version` - Target version: 4 or 5 (default: 5)
**Examples:**
```bash
# Analyze inline code (escape $ as \$)
npx @sveltejs/mcp svelte-autofixer '<script>let count = \$state(0);</script>'
# Analyze a file
npx @sveltejs/mcp svelte-autofixer ./src/lib/Component.svelte
# Target Svelte 4
npx @sveltejs/mcp svelte-autofixer ./Component.svelte --svelte-version 4
```
**Important:** When passing code with runes (`$state`, `$derived`, etc.) via the terminal, escape the `$` character as `\$` to prevent shell variable substitution.
## Workflow
1. **Uncertain about syntax?** Run `list-sections` then `get-documentation` for relevant topics
2. **Reviewing/debugging?** Run `svelte-autofixer` on the code to detect issues
3. **Always validate** - Run `svelte-autofixer` before finalizing any Svelte component

201
.github/agents/Svelte.agent.md vendored Normal file
View file

@ -0,0 +1,201 @@
---
name: Svelte
description: "Use when: building Svelte components, writing SvelteKit routes, debugging Svelte reactivity, migrating from Svelte 4 to 5, using runes ($state, $derived, $effect, $props), creating stores, animations, transitions, form actions, or any Svelte/SvelteKit question."
argument-hint: "Describe the Svelte component or SvelteKit feature you want to build, fix, or explain."
tools: [read, edit, search, todo, mcp_svelte_offici/*]
---
You are a Svelte expert tasked to build components and utilities for Svelte developers. If you need documentation for anything related to Svelte you can invoke the tool `get-documentation` with one of the following paths. However: before invoking the `get-documentation` tool, try to answer the users query using your own knowledge and the `svelte-autofixer` tool. Be mindful of how many section you request, since it is token-intensive!
<available-docs>
- title: Overview, use_cases: use title and path to estimate use case, path: ai/overview
- title: Local setup, use_cases: use title and path to estimate use case, path: ai/local-setup
- title: Remote setup, use_cases: use title and path to estimate use case, path: ai/remote-setup
- title: Tools, use_cases: use title and path to estimate use case, path: ai/tools
- title: Resources, use_cases: use title and path to estimate use case, path: ai/resources
- title: Prompts, use_cases: use title and path to estimate use case, path: ai/prompts
- title: Overview, use_cases: use title and path to estimate use case, path: ai/plugin
- title: Subagent, use_cases: use title and path to estimate use case, path: ai/subagent
- title: Overview, use_cases: use title and path to estimate use case, path: ai/opencode-plugin
- title: Subagent, use_cases: use title and path to estimate use case, path: ai/opencode-subagent
- title: Overview, use_cases: use title and path to estimate use case, path: ai/skills
- title: Overview, use_cases: project setup, creating new svelte apps, scaffolding, cli tools, initializing projects, path: cli/overview
- title: Frequently asked questions, use_cases: project setup, initializing new svelte projects, troubleshooting cli installation, package manager configuration, path: cli/faq
- title: sv create, use_cases: project setup, starting new sveltekit app, initializing project, creating from playground, choosing project template, path: cli/sv-create
- title: sv add, use_cases: project setup, adding features to existing projects, integrating tools, testing setup, styling setup, authentication, database setup, deployment adapters, path: cli/sv-add
- title: sv check, use_cases: code quality, ci/cd pipelines, error checking, typescript projects, pre-commit hooks, finding unused css, accessibility auditing, production builds, path: cli/sv-check
- title: sv migrate, use_cases: migration, upgrading svelte versions, upgrading sveltekit versions, modernizing codebase, svelte 3 to 4, svelte 4 to 5, sveltekit 1 to 2, adopting runes, refactoring deprecated apis, path: cli/sv-migrate
- title: devtools-json, use_cases: development setup, chrome devtools integration, browser-based editing, local development workflow, debugging setup, path: cli/devtools-json
- title: drizzle, use_cases: database setup, sql queries, orm integration, data modeling, postgresql, mysql, sqlite, server-side data access, database migrations, type-safe queries, path: cli/drizzle
- title: eslint, use_cases: code quality, linting, error detection, project setup, code standards, team collaboration, typescript projects, path: cli/eslint
- title: better-auth, use_cases: use title and path to estimate use case, path: cli/better-auth
- title: mcp, use_cases: use title and path to estimate use case, path: cli/mcp
- title: mdsvex, use_cases: blog, content sites, markdown rendering, documentation sites, technical writing, cms integration, article pages, path: cli/mdsvex
- title: paraglide, use_cases: internationalization, multi-language sites, i18n, translation, localization, language switching, global apps, multilingual content, path: cli/paraglide
- title: playwright, use_cases: browser testing, e2e testing, integration testing, test automation, quality assurance, ci/cd pipelines, testing user flows, path: cli/playwright
- title: prettier, use_cases: code formatting, project setup, code style consistency, team collaboration, linting configuration, path: cli/prettier
- title: storybook, use_cases: component development, design systems, ui library, isolated component testing, documentation, visual testing, component showcase, path: cli/storybook
- title: sveltekit-adapter, use_cases: deployment, production builds, hosting setup, choosing deployment platform, configuring adapters, static site generation, node server, vercel, cloudflare, netlify, path: cli/sveltekit-adapter
- title: tailwindcss, use_cases: project setup, styling, css framework, rapid prototyping, utility-first css, design systems, responsive design, adding tailwind to svelte, path: cli/tailwind
- title: vitest, use_cases: testing, unit tests, component testing, test setup, quality assurance, ci/cd pipelines, test-driven development, path: cli/vitest
- title: add-on, use_cases: use title and path to estimate use case, path: cli/add-on
- title: sv-utils, use_cases: use title and path to estimate use case, path: cli/sv-utils
- title: Introduction, use_cases: learning sveltekit, project setup, understanding framework basics, choosing between svelte and sveltekit, getting started with full-stack apps, path: kit/introduction
- title: Creating a project, use_cases: project setup, starting new sveltekit app, initial development environment, first-time sveltekit users, scaffolding projects, path: kit/creating-a-project
- title: Project types, use_cases: deployment, project setup, choosing adapters, ssg, spa, ssr, serverless, mobile apps, desktop apps, pwa, offline apps, browser extensions, separate backend, docker containers, path: kit/project-types
- title: Project structure, use_cases: project setup, understanding file structure, organizing code, starting new project, learning sveltekit basics, path: kit/project-structure
- title: Web standards, use_cases: always, any sveltekit project, data fetching, forms, api routes, server-side rendering, deployment to various platforms, path: kit/web-standards
- title: Routing, use_cases: routing, navigation, multi-page apps, project setup, file structure, api endpoints, data loading, layouts, error pages, always, path: kit/routing
- title: Loading data, use_cases: data fetching, api calls, database queries, dynamic routes, page initialization, loading states, authentication checks, ssr data, form data, content rendering, path: kit/load
- title: Form actions, use_cases: forms, user input, data submission, authentication, login systems, user registration, progressive enhancement, validation errors, path: kit/form-actions
- title: Page options, use_cases: prerendering static sites, ssr configuration, spa setup, client-side rendering control, url trailing slash handling, adapter deployment config, build optimization, path: kit/page-options
- title: State management, use_cases: sveltekit, server-side rendering, ssr, state management, authentication, data persistence, load functions, context api, navigation, component lifecycle, path: kit/state-management
- title: Remote functions, use_cases: data fetching, server-side logic, database queries, type-safe client-server communication, forms, user input, mutations, authentication, crud operations, optimistic updates, path: kit/remote-functions
- title: Building your app, use_cases: production builds, deployment preparation, build process optimization, adapter configuration, preview before deployment, path: kit/building-your-app
- title: Adapters, use_cases: deployment, production builds, hosting setup, choosing deployment platform, configuring adapters, path: kit/adapters
- title: Zero-config deployments, use_cases: deployment, production builds, hosting setup, choosing deployment platform, ci/cd configuration, path: kit/adapter-auto
- title: Node servers, use_cases: deployment, production builds, node.js hosting, custom server setup, environment configuration, reverse proxy setup, docker deployment, systemd services, path: kit/adapter-node
- title: Static site generation, use_cases: static site generation, ssg, prerendering, deployment, github pages, spa mode, blogs, documentation sites, marketing sites, path: kit/adapter-static
- title: Single-page apps, use_cases: spa mode, single-page apps, client-only rendering, static hosting, mobile app wrappers, no server-side logic, adapter-static setup, fallback pages, path: kit/single-page-apps
- title: Cloudflare, use_cases: deployment, cloudflare workers, cloudflare pages, hosting setup, production builds, serverless deployment, edge computing, path: kit/adapter-cloudflare
- title: Cloudflare Workers, use_cases: deploying to cloudflare workers, cloudflare workers sites deployment, legacy cloudflare adapter, wrangler configuration, cloudflare platform bindings, path: kit/adapter-cloudflare-workers
- title: Netlify, use_cases: deployment, netlify hosting, production builds, serverless functions, edge functions, static site hosting, path: kit/adapter-netlify
- title: Vercel, use_cases: deployment, vercel hosting, production builds, serverless functions, edge functions, isr, image optimization, environment variables, path: kit/adapter-vercel
- title: Writing adapters, use_cases: custom deployment, building adapters, unsupported platforms, adapter development, custom hosting environments, path: kit/writing-adapters
- title: Advanced routing, use_cases: advanced routing, dynamic routes, file viewers, nested paths, custom 404 pages, url validation, route parameters, multi-level navigation, path: kit/advanced-routing
- title: Hooks, use_cases: authentication, logging, error tracking, request interception, api proxying, custom routing, internationalization, database initialization, middleware logic, session management, path: kit/hooks
- title: Errors, use_cases: error handling, custom error pages, 404 pages, api error responses, production error logging, error tracking, type-safe errors, path: kit/errors
- title: Link options, use_cases: routing, navigation, multi-page apps, performance optimization, link preloading, forms with get method, search functionality, focus management, scroll behavior, path: kit/link-options
- title: Service workers, use_cases: offline support, pwa, caching strategies, performance optimization, precaching assets, network resilience, progressive web apps, path: kit/service-workers
- title: Server-only modules, use_cases: api keys, environment variables, sensitive data protection, backend security, preventing data leaks, server-side code isolation, path: kit/server-only-modules
- title: Snapshots, use_cases: forms, user input, preserving form data, multi-step forms, navigation state, preventing data loss, textarea content, input fields, comment systems, surveys, path: kit/snapshots
- title: Shallow routing, use_cases: modals, dialogs, image galleries, overlays, history-driven ui, mobile-friendly navigation, photo viewers, lightboxes, drawer menus, path: kit/shallow-routing
- title: Observability, use_cases: performance monitoring, debugging, observability, tracing requests, production diagnostics, analyzing slow requests, finding bottlenecks, monitoring server-side operations, path: kit/observability
- title: Packaging, use_cases: building component libraries, publishing npm packages, creating reusable svelte components, library development, package distribution, path: kit/packaging
- title: Auth, use_cases: authentication, login systems, user management, session handling, jwt tokens, protected routes, user credentials, authorization checks, path: kit/auth
- title: Performance, use_cases: performance optimization, slow loading pages, production deployment, debugging performance issues, reducing bundle size, improving load times, path: kit/performance
- title: Icons, use_cases: icons, ui components, styling, css frameworks, tailwind, unocss, performance optimization, dependency management, path: kit/icons
- title: Images, use_cases: image optimization, responsive images, performance, hero images, product photos, galleries, cms integration, cdn setup, asset management, path: kit/images
- title: Accessibility, use_cases: always, any sveltekit project, screen reader support, keyboard navigation, multi-page apps, client-side routing, internationalization, multilingual sites, path: kit/accessibility
- title: SEO, use_cases: seo optimization, search engine ranking, content sites, blogs, marketing sites, public-facing apps, sitemaps, amp pages, meta tags, performance optimization, path: kit/seo
- title: Frequently asked questions, use_cases: troubleshooting package imports, library compatibility issues, client-side code execution, external api integration, middleware setup, database configuration, view transitions, yarn configuration, path: kit/faq
- title: Integrations, use_cases: project setup, css preprocessors, postcss, scss, sass, less, stylus, typescript setup, adding integrations, tailwind, testing, auth, linting, formatting, path: kit/integrations
- title: Breakpoint Debugging, use_cases: debugging, breakpoints, development workflow, troubleshooting issues, vscode setup, ide configuration, inspecting code execution, path: kit/debugging
- title: Migrating to SvelteKit v2, use_cases: migration, upgrading from sveltekit 1 to 2, breaking changes, version updates, path: kit/migrating-to-sveltekit-2
- title: Migrating from Sapper, use_cases: migrating from sapper, upgrading legacy projects, sapper to sveltekit conversion, project modernization, path: kit/migrating
- title: Additional resources, use_cases: troubleshooting, getting help, finding examples, learning sveltekit, project templates, common issues, community support, path: kit/additional-resources
- title: Glossary, use_cases: rendering strategies, performance optimization, deployment configuration, seo requirements, static sites, spas, server-side rendering, prerendering, edge deployment, pwa development, path: kit/glossary
- title: @sveltejs/kit, use_cases: forms, form actions, server-side validation, form submission, error handling, redirects, json responses, http errors, server utilities, path: kit/@sveltejs-kit
- title: @sveltejs/kit/hooks, use_cases: middleware, request processing, authentication chains, logging, multiple hooks, request/response transformation, path: kit/@sveltejs-kit-hooks
- title: @sveltejs/kit/node/polyfills, use_cases: node.js environments, custom servers, non-standard runtimes, ssr setup, web api compatibility, polyfill requirements, path: kit/@sveltejs-kit-node-polyfills
- title: @sveltejs/kit/node, use_cases: node.js adapter, custom server setup, http integration, streaming files, node deployment, server-side rendering with node, path: kit/@sveltejs-kit-node
- title: @sveltejs/kit/vite, use_cases: project setup, vite configuration, initial sveltekit setup, build tooling, path: kit/@sveltejs-kit-vite
- title: $app/environment, use_cases: always, conditional logic, client-side code, server-side code, build-time logic, prerendering, development vs production, environment detection, path: kit/$app-environment
- title: $app/forms, use_cases: forms, user input, data submission, progressive enhancement, custom form handling, form validation, path: kit/$app-forms
- title: $app/navigation, use_cases: routing, navigation, multi-page apps, programmatic navigation, data reloading, preloading, shallow routing, navigation lifecycle, scroll handling, view transitions, path: kit/$app-navigation
- title: $app/paths, use_cases: static assets, images, fonts, public files, base path configuration, subdirectory deployment, cdn setup, asset urls, links, navigation, path: kit/$app-paths
- title: $app/server, use_cases: remote functions, server-side logic, data fetching, form handling, api endpoints, client-server communication, prerendering, file reading, batch queries, path: kit/$app-server
- title: $app/state, use_cases: routing, navigation, multi-page apps, loading states, url parameters, form handling, error states, version updates, page metadata, shallow routing, path: kit/$app-state
- title: $app/stores, use_cases: legacy projects, sveltekit pre-2.12, migration from stores to runes, maintaining older codebases, accessing page data, navigation state, app version updates, path: kit/$app-stores
- title: $app/types, use_cases: routing, navigation, type safety, route parameters, dynamic routes, link generation, pathname validation, multi-page apps, path: kit/$app-types
- title: $env/dynamic/private, use_cases: api keys, secrets management, server-side config, environment variables, backend logic, deployment-specific settings, private data handling, path: kit/$env-dynamic-private
- title: $env/dynamic/public, use_cases: environment variables, client-side config, runtime configuration, public api keys, deployment-specific settings, multi-environment apps, path: kit/$env-dynamic-public
- title: $env/static/private, use_cases: server-side api keys, backend secrets, database credentials, private configuration, build-time optimization, server endpoints, authentication tokens, path: kit/$env-static-private
- title: $env/static/public, use_cases: environment variables, public config, client-side data, api endpoints, build-time configuration, public constants, path: kit/$env-static-public
- title: $lib, use_cases: project setup, component organization, importing shared components, reusable ui elements, code structure, path: kit/$lib
- title: $service-worker, use_cases: offline support, pwa, service workers, caching strategies, progressive web apps, offline-first apps, path: kit/$service-worker
- title: Configuration, use_cases: project setup, configuration, adapters, deployment, build settings, environment variables, routing customization, prerendering, csp security, csrf protection, path configuration, typescript setup, path: kit/configuration
- title: Command Line Interface, use_cases: project setup, typescript configuration, generated types, ./$types imports, initial project configuration, path: kit/cli
- title: Types, use_cases: typescript, type safety, route parameters, api endpoints, load functions, form actions, generated types, jsconfig setup, path: kit/types
- title: Overview, use_cases: always, any svelte project, getting started, learning svelte, introduction, project setup, understanding framework basics, path: svelte/overview
- title: Getting started, use_cases: project setup, starting new svelte project, initial installation, choosing between sveltekit and vite, editor configuration, path: svelte/getting-started
- title: .svelte files, use_cases: always, any svelte project, component creation, project setup, learning svelte basics, path: svelte/svelte-files
- title: .svelte.js and .svelte.ts files, use_cases: shared reactive state, reusable reactive logic, state management across components, global stores, custom reactive utilities, path: svelte/svelte-js-files
- title: What are runes?, use_cases: always, any svelte 5 project, understanding core syntax, learning svelte 5, migration from svelte 4, path: svelte/what-are-runes
- title: $state, use_cases: always, any svelte project, core reactivity, state management, counters, forms, todo apps, interactive ui, data updates, class-based components, path: svelte/$state
- title: $derived, use_cases: always, any svelte project, computed values, reactive calculations, derived data, transforming state, dependent values, path: svelte/$derived
- title: $effect, use_cases: canvas drawing, third-party library integration, dom manipulation, side effects, intervals, timers, network requests, analytics tracking, path: svelte/$effect
- title: $props, use_cases: always, any svelte project, passing data to components, component communication, reusable components, component props, path: svelte/$props
- title: $bindable, use_cases: forms, user input, two-way data binding, custom input components, parent-child communication, reusable form fields, path: svelte/$bindable
- title: $inspect, use_cases: debugging, development, tracking state changes, reactive state monitoring, troubleshooting reactivity issues, path: svelte/$inspect
- title: $host, use_cases: custom elements, web components, dispatching custom events, component library, framework-agnostic components, path: svelte/$host
- title: Basic markup, use_cases: always, any svelte project, basic markup, html templating, component structure, attributes, events, props, text rendering, path: svelte/basic-markup
- title: {#if ...}, use_cases: always, conditional rendering, showing/hiding content, dynamic ui, user permissions, loading states, error handling, form validation, path: svelte/if
- title: {#each ...}, use_cases: always, lists, arrays, iteration, product listings, todos, tables, grids, dynamic content, shopping carts, user lists, comments, feeds, path: svelte/each
- title: {#key ...}, use_cases: animations, transitions, component reinitialization, forcing component remount, value-based ui updates, resetting component state, path: svelte/key
- title: {#await ...}, use_cases: async data fetching, api calls, loading states, promises, error handling, lazy loading components, dynamic imports, path: svelte/await
- title: {#snippet ...}, use_cases: reusable markup, component composition, passing content to components, table rows, list items, conditional rendering, reducing duplication, path: svelte/snippet
- title: {@render ...}, use_cases: reusable ui patterns, component composition, conditional rendering, fallback content, layout components, slot alternatives, template reuse, path: svelte/@render
- title: {@html ...}, use_cases: rendering html strings, cms content, rich text editors, markdown to html, blog posts, wysiwyg output, sanitized html injection, dynamic html content, path: svelte/@html
- title: {@attach ...}, use_cases: tooltips, popovers, dom manipulation, third-party libraries, canvas drawing, element lifecycle, interactive ui, custom directives, wrapper components, path: svelte/@attach
- title: {@const ...}, use_cases: computed values in loops, derived calculations in blocks, local variables in each iterations, complex list rendering, path: svelte/@const
- title: {@debug ...}, use_cases: debugging, development, troubleshooting, tracking state changes, monitoring variables, reactive data inspection, path: svelte/@debug
- title: bind:, use_cases: forms, user input, two-way data binding, interactive ui, media players, file uploads, checkboxes, radio buttons, select dropdowns, contenteditable, dimension tracking, path: svelte/bind
- title: use:, use_cases: custom directives, dom manipulation, third-party library integration, tooltips, click outside, gestures, focus management, element lifecycle hooks, path: svelte/use
- title: transition:, use_cases: animations, interactive ui, modals, dropdowns, notifications, conditional content, show/hide elements, smooth state changes, path: svelte/transition
- title: in: and out:, use_cases: animation, transitions, interactive ui, conditional rendering, independent enter/exit effects, modals, tooltips, notifications, path: svelte/in-and-out
- title: animate:, use_cases: sortable lists, drag and drop, reorderable items, todo lists, kanban boards, playlist editors, priority queues, animated list reordering, path: svelte/animate
- title: style:, use_cases: dynamic styling, conditional styles, theming, dark mode, responsive design, interactive ui, component styling, path: svelte/style
- title: class, use_cases: always, conditional styling, dynamic classes, tailwind css, component styling, reusable components, responsive design, path: svelte/class
- title: await, use_cases: async data fetching, loading states, server-side rendering, awaiting promises in components, async validation, concurrent data loading, path: svelte/await-expressions
- title: Scoped styles, use_cases: always, styling components, scoped css, component-specific styles, preventing style conflicts, animations, keyframes, path: svelte/scoped-styles
- title: Global styles, use_cases: global styles, third-party libraries, css resets, animations, styling body/html, overriding component styles, shared keyframes, base styles, path: svelte/global-styles
- title: Custom properties, use_cases: theming, custom styling, reusable components, design systems, dynamic colors, component libraries, ui customization, path: svelte/custom-properties
- title: Nested <style> elements, use_cases: component styling, scoped styles, dynamic styles, conditional styling, nested style tags, custom styling logic, path: svelte/nested-style-elements
- title: <svelte:boundary>, use_cases: error handling, async data loading, loading states, error recovery, flaky components, error reporting, resilient ui, path: svelte/svelte-boundary
- title: <svelte:window>, use_cases: keyboard shortcuts, scroll tracking, window resize handling, responsive layouts, online/offline detection, viewport dimensions, global event listeners, path: svelte/svelte-window
- title: <svelte:document>, use_cases: document events, visibility tracking, fullscreen detection, pointer lock, focus management, document-level interactions, path: svelte/svelte-document
- title: <svelte:body>, use_cases: mouse tracking, hover effects, cursor interactions, global body events, drag and drop, custom cursors, interactive backgrounds, body-level actions, path: svelte/svelte-body
- title: <svelte:head>, use_cases: seo optimization, page titles, meta tags, social media sharing, dynamic head content, multi-page apps, blog posts, product pages, path: svelte/svelte-head
- title: <svelte:element>, use_cases: dynamic content, cms integration, user-generated content, configurable ui, runtime element selection, flexible components, path: svelte/svelte-element
- title: <svelte:options>, use_cases: migration, custom elements, web components, legacy mode compatibility, runes mode setup, svg components, mathml components, css injection control, path: svelte/svelte-options
- title: Stores, use_cases: shared state, cross-component data, reactive values, async data streams, manual control over updates, rxjs integration, extracting logic, path: svelte/stores
- title: Context, use_cases: shared state, avoiding prop drilling, component communication, theme providers, user context, authentication state, configuration sharing, deeply nested components, path: svelte/context
- title: Lifecycle hooks, use_cases: component initialization, cleanup tasks, timers, subscriptions, dom measurements, chat windows, autoscroll features, migration from svelte 4, path: svelte/lifecycle-hooks
- title: Imperative component API, use_cases: project setup, client-side rendering, server-side rendering, ssr, hydration, testing, programmatic component creation, tooltips, dynamic mounting, path: svelte/imperative-component-api
- title: Hydratable data, use_cases: use title and path to estimate use case, path: svelte/hydratable
- title: Best practices, use_cases: use title and path to estimate use case, path: svelte/best-practices
- title: Testing, use_cases: testing, quality assurance, unit tests, integration tests, component tests, e2e tests, vitest setup, playwright setup, test automation, path: svelte/testing
- title: TypeScript, use_cases: typescript setup, type safety, component props typing, generic components, wrapper components, dom type augmentation, project configuration, path: svelte/typescript
- title: Custom elements, use_cases: web components, custom elements, component library, design system, framework-agnostic components, embedding svelte in non-svelte apps, shadow dom, path: svelte/custom-elements
- title: Svelte 4 migration guide, use_cases: upgrading svelte 3 to 4, version migration, updating dependencies, breaking changes, legacy project maintenance, path: svelte/v4-migration-guide
- title: Svelte 5 migration guide, use_cases: migrating from svelte 4 to 5, upgrading projects, learning svelte 5 syntax changes, runes migration, event handler updates, path: svelte/v5-migration-guide
- title: Frequently asked questions, use_cases: getting started, learning svelte, beginner setup, project initialization, vs code setup, formatting, testing, routing, mobile apps, troubleshooting, community support, path: svelte/faq
- title: svelte, use_cases: migration from svelte 4 to 5, upgrading legacy code, component lifecycle hooks, context api, mounting components, event dispatchers, typescript component types, path: svelte/svelte
- title: svelte/action, use_cases: typescript types, actions, use directive, dom manipulation, element lifecycle, custom behaviors, third-party library integration, path: svelte/svelte-action
- title: svelte/animate, use_cases: animated lists, sortable items, drag and drop, reordering elements, todo lists, kanban boards, playlist management, smooth position transitions, path: svelte/svelte-animate
- title: svelte/attachments, use_cases: library development, component libraries, programmatic element manipulation, migrating from actions to attachments, spreading props onto elements, path: svelte/svelte-attachments
- title: svelte/compiler, use_cases: build tools, custom compilers, ast manipulation, preprocessors, code transformation, migration scripts, syntax analysis, bundler plugins, dev tools, path: svelte/svelte-compiler
- title: svelte/easing, use_cases: animations, transitions, custom easing, smooth motion, interactive ui, modals, dropdowns, carousels, page transitions, scroll effects, path: svelte/svelte-easing
- title: svelte/events, use_cases: window events, document events, global event listeners, event delegation, programmatic event handling, cleanup functions, media queries, path: svelte/svelte-events
- title: svelte/legacy, use_cases: migration from svelte 4 to svelte 5, upgrading legacy code, event modifiers, class components, imperative component instantiation, path: svelte/svelte-legacy
- title: svelte/motion, use_cases: animation, smooth transitions, interactive ui, sliders, counters, physics-based motion, drag gestures, accessibility, reduced motion, path: svelte/svelte-motion
- title: svelte/reactivity/window, use_cases: responsive design, viewport tracking, scroll effects, window resize handling, online/offline detection, zoom level tracking, path: svelte/svelte-reactivity-window
- title: svelte/reactivity, use_cases: reactive data structures, state management with maps/sets, game boards, selection tracking, url manipulation, query params, real-time clocks, media queries, responsive design, path: svelte/svelte-reactivity
- title: svelte/server, use_cases: server-side rendering, ssr, static site generation, seo optimization, initial page load, pre-rendering, node.js server, custom server setup, path: svelte/svelte-server
- title: svelte/store, use_cases: state management, shared data, reactive stores, cross-component communication, global state, computed values, data synchronization, legacy svelte projects, path: svelte/svelte-store
- title: svelte/transition, use_cases: animations, transitions, interactive ui, modals, dropdowns, tooltips, notifications, svg animations, list animations, page transitions, path: svelte/svelte-transition
- title: Compiler errors, use_cases: animation, transitions, keyed each blocks, list animations, path: svelte/compiler-errors
- title: Compiler warnings, use_cases: accessibility, a11y compliance, wcag standards, screen readers, keyboard navigation, aria attributes, semantic html, interactive elements, path: svelte/compiler-warnings
- title: Runtime errors, use_cases: debugging errors, error handling, troubleshooting runtime issues, migration to svelte 5, component binding, effects and reactivity, path: svelte/runtime-errors
- title: Runtime warnings, use_cases: debugging state proxies, console logging reactive values, inspecting state changes, development troubleshooting, path: svelte/runtime-warnings
- title: Overview, use_cases: migrating from svelte 3/4 to svelte 5, maintaining legacy components, understanding deprecated features, gradual upgrade process, path: svelte/legacy-overview
- title: Reactive let/var declarations, use_cases: migration, legacy svelte projects, upgrading from svelte 4, understanding old reactivity, maintaining existing code, learning runes differences, path: svelte/legacy-let
- title: Reactive $: statements, use_cases: legacy mode, migration from svelte 4, reactive statements, computed values, derived state, side effects, path: svelte/legacy-reactive-assignments
- title: export let, use_cases: legacy mode, migration from svelte 4, maintaining older projects, component props without runes, exporting component methods, renaming reserved word props, path: svelte/legacy-export-let
- title: $$props and $$restProps, use_cases: legacy mode migration, component wrappers, prop forwarding, button components, reusable ui components, spreading props to child elements, path: svelte/legacy-$$props-and-$$restProps
- title: on:, use_cases: legacy mode, event handling, button clicks, forms, user interactions, component communication, event forwarding, event modifiers, path: svelte/legacy-on
- title: <slot>, use_cases: legacy mode, migrating from svelte 4, component composition, reusable components, passing content to components, modals, layouts, wrappers, path: svelte/legacy-slots
- title: $$slots, use_cases: legacy mode, conditional slot rendering, optional content sections, checking if slots provided, migrating from legacy to runes, path: svelte/legacy-$$slots
- title: <svelte:fragment>, use_cases: named slots, component composition, layout systems, avoiding wrapper divs, legacy svelte projects, slot content organization, path: svelte/legacy-svelte-fragment
- title: <svelte:component>, use_cases: dynamic components, component switching, conditional rendering, legacy mode migration, tabbed interfaces, multi-step forms, path: svelte/legacy-svelte-component
- title: <svelte:self>, use_cases: recursive components, tree structures, nested menus, file explorers, comment threads, hierarchical data, path: svelte/legacy-svelte-self
- title: Imperative component API, use_cases: migration from svelte 3/4 to 5, legacy component api, maintaining old projects, understanding deprecated patterns, path: svelte/legacy-component-api
</available-docs>
These are the available documentation sections that `list-sections` will return, you do not need to call it again.
Every time you write a Svelte component or a Svelte module you MUST invoke the `svelte-autofixer` tool providing the code. The tool will return a list of issues or suggestions. If there are any issues or suggestions you MUST fix them and call the tool again with the updated code. You MUST keep doing this until the tool returns no issues or suggestions. Only then you can return the code to the user.
If you are not writing the code into a file, once you have the final version of the code ask the user if it wants to generate a playground link to quickly check the code in it and if it answer yes call the `playground-link` tool and return the url to the user nicely formatted. The playground link MUST be generated only once you have the final version of the code and you are ready to share it, it MUST include an entry point file called `App.svelte` where the main component should live. If you have multiple files to include in the playground link you can include them all at the root.

View file

@ -2,3 +2,10 @@
## (And the starting over thereof) ## (And the starting over thereof)
We'll build this by hand, one part at a time. We'll build this by hand, one part at a time.
## Usage
```bash
pnpm install
pnpm run dev
```

View file

@ -1,12 +1,25 @@
// @ts-check // @ts-check
import { defineConfig } from 'astro/config'; import { defineConfig } from 'astro/config';
import path from 'path';
import svelte from '@astrojs/svelte'; import svelte from '@astrojs/svelte';
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
integrations: [svelte()], vite: {
devToolbar: { resolve: {
enabled: false alias: {
'@components': path.resolve('./src/components'),
} }
},
css: {
preprocessorOptions: {
scss: {
loadPaths: [path.resolve('./src/styles')]
}
}
}
},
integrations: [svelte()],
devToolbar: { enabled: false }
}); });

View file

@ -16,7 +16,12 @@
"@reuters-graphics/svelte-markdown": "^0.0.3", "@reuters-graphics/svelte-markdown": "^0.0.3",
"@rferl/veronica": "github:rferl/veronica", "@rferl/veronica": "github:rferl/veronica",
"astro": "^6.3.1", "astro": "^6.3.1",
"journalize": "^2.6.0",
"maplibre-gl": "^5.24.0",
"pmtiles": "^4.3.0",
"slugify": "^1.6.9",
"svelte": "^5.55.5", "svelte": "^5.55.5",
"svelte-fa": "^4.0.4",
"typescript": "^5.9.3" "typescript": "^5.9.3"
}, },
"devDependencies": { "devDependencies": {

View file

@ -20,9 +20,24 @@ importers:
astro: astro:
specifier: ^6.3.1 specifier: ^6.3.1
version: 6.3.1(rollup@4.60.3)(sass-embedded@1.99.0)(sass@1.99.0) version: 6.3.1(rollup@4.60.3)(sass-embedded@1.99.0)(sass@1.99.0)
journalize:
specifier: ^2.6.0
version: 2.6.0
maplibre-gl:
specifier: ^5.24.0
version: 5.24.0
pmtiles:
specifier: ^4.3.0
version: 4.4.1
slugify:
specifier: ^1.6.9
version: 1.6.9
svelte: svelte:
specifier: ^5.55.5 specifier: ^5.55.5
version: 5.55.5 version: 5.55.5
svelte-fa:
specifier: ^4.0.4
version: 4.0.4(svelte@5.55.5)
typescript: typescript:
specifier: ^5.9.3 specifier: ^5.9.3
version: 5.9.3 version: 5.9.3
@ -418,6 +433,42 @@ packages:
'@jridgewell/trace-mapping@0.3.31': '@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@mapbox/jsonlint-lines-primitives@2.0.2':
resolution: {integrity: sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==}
engines: {node: '>= 0.6'}
'@mapbox/point-geometry@1.1.0':
resolution: {integrity: sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==}
'@mapbox/tiny-sdf@2.2.0':
resolution: {integrity: sha512-LVL4wgI9YAum5V+LNVQO6QgFBPw7/MIIY4XJPNsPDMrjEwcE+JfKk1LuIl8GnF197ejVdC9QdPaxrx5gfgdGXg==}
'@mapbox/unitbezier@0.0.1':
resolution: {integrity: sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==}
'@mapbox/vector-tile@2.0.4':
resolution: {integrity: sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==}
'@mapbox/whoots-js@3.1.0':
resolution: {integrity: sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==}
engines: {node: '>=6.0.0'}
'@maplibre/geojson-vt@5.0.4':
resolution: {integrity: sha512-KGg9sma45S+stfH9vPCJk1J0lSDLWZgCT9Y8u8qWZJyjFlP8MNP1WGTxIMYJZjDvVT3PDn05kN1C95Sut1HpgQ==}
'@maplibre/geojson-vt@6.1.0':
resolution: {integrity: sha512-2eIY4gZxeKIVOZVNkAMb+5NgXhgsMQpOveTQAvnp53LYqHGJZDidk7Ew0Tged9PThidpbS+NFTh0g4zivhPDzQ==}
'@maplibre/maplibre-gl-style-spec@24.8.5':
resolution: {integrity: sha512-EzEJmMt6thioRH7GI9LWS7ahXTcAhAPGWCe6oTP2Ps4YnsXOOAfeqx854lZaiDnwURfHmcCKV1mr6oo0i23x6w==}
hasBin: true
'@maplibre/mlt@1.1.9':
resolution: {integrity: sha512-g/tD8EYJB97udq33ipuJ9a4Q7fcbZnTEnUrgnEc/tLMmEL+zaCbR+X5fkDBO2dgpaAMsLH179qE3UXg2N0Nc/g==}
'@maplibre/vt-pbf@4.3.0':
resolution: {integrity: sha512-jIvp8F5hQCcreqOOpEt42TJMUlsrEcpf/kI1T2v85YrQRV6PPXUcEXUg5karKtH6oh47XJZ4kHu56pUkOuqA7w==}
'@oslojs/encoding@1.1.0': '@oslojs/encoding@1.1.0':
resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
@ -725,6 +776,9 @@ packages:
'@types/estree@1.0.9': '@types/estree@1.0.9':
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
'@types/geojson@7946.0.16':
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
'@types/hast@3.0.4': '@types/hast@3.0.4':
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
@ -737,6 +791,9 @@ packages:
'@types/nlcst@2.0.3': '@types/nlcst@2.0.3':
resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==}
'@types/supercluster@7.1.3':
resolution: {integrity: sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==}
'@types/trusted-types@2.0.7': '@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
@ -915,6 +972,9 @@ packages:
resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==}
engines: {node: '>=4'} engines: {node: '>=4'}
earcut@3.0.2:
resolution: {integrity: sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==}
entities@4.5.0: entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'} engines: {node: '>=0.12'}
@ -976,6 +1036,9 @@ packages:
picomatch: picomatch:
optional: true optional: true
fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
flattie@1.1.1: flattie@1.1.1:
resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -999,6 +1062,9 @@ packages:
github-slugger@2.0.0: github-slugger@2.0.0:
resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
gl-matrix@3.4.4:
resolution: {integrity: sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==}
h3@1.15.11: h3@1.15.11:
resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==} resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==}
@ -1085,13 +1151,22 @@ packages:
resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==}
engines: {node: '>=16'} engines: {node: '>=16'}
journalize@2.6.0:
resolution: {integrity: sha512-9Fi36vKj8MRtmqThQf8AK0GnTc1IYKCftyqwJA61dKNnkH82we2qqIPYDl1UX6farfQMCZBEV96OoEkDDQVY8g==}
js-yaml@4.1.1: js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true hasBin: true
json-stringify-pretty-compact@4.0.0:
resolution: {integrity: sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==}
jsonc-parser@3.3.1: jsonc-parser@3.3.1:
resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==}
kdbush@4.0.2:
resolution: {integrity: sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==}
locate-character@3.0.0: locate-character@3.0.0:
resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
@ -1108,6 +1183,10 @@ packages:
magicast@0.5.2: magicast@0.5.2:
resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==} resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==}
maplibre-gl@5.24.0:
resolution: {integrity: sha512-ALyFxgtd5R+65UqZ/++lOqwWcC0SNho9c27fYSyLmG7AfnAul2o46F05aDJGPbFU57wos9dgcIySHs0Xe6ia3A==}
engines: {node: '>=16.14.0', npm: '>=8.1.0'}
markdown-table@3.0.4: markdown-table@3.0.4:
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
@ -1250,6 +1329,9 @@ packages:
micromark@4.0.2: micromark@4.0.2:
resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==}
minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
mrmime@2.0.1: mrmime@2.0.1:
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -1257,6 +1339,9 @@ packages:
ms@2.1.3: ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
murmurhash-js@1.0.0:
resolution: {integrity: sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==}
nanoid@3.3.12: nanoid@3.3.12:
resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@ -1321,6 +1406,10 @@ packages:
parse5@7.3.0: parse5@7.3.0:
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
pbf@4.0.1:
resolution: {integrity: sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==}
hasBin: true
piccolore@0.1.3: piccolore@0.1.3:
resolution: {integrity: sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==} resolution: {integrity: sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==}
@ -1335,10 +1424,16 @@ packages:
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
engines: {node: '>=12'} engines: {node: '>=12'}
pmtiles@4.4.1:
resolution: {integrity: sha512-5oTeQc/yX/ft1evbpIlnoCZugQuug/iYIAj/ZTqIqzdGek4uZEho99En890EE6NOSI3JTI3IG8R7r8+SltphxA==}
postcss@8.5.14: postcss@8.5.14:
resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
potpack@2.1.0:
resolution: {integrity: sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==}
prismjs@1.30.0: prismjs@1.30.0:
resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -1346,6 +1441,12 @@ packages:
property-information@7.1.0: property-information@7.1.0:
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
protocol-buffers-schema@3.6.1:
resolution: {integrity: sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==}
quickselect@3.0.0:
resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==}
radix3@1.1.2: radix3@1.1.2:
resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==}
@ -1397,6 +1498,9 @@ packages:
resolve-pkg-maps@1.0.0: resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
resolve-protobuf-schema@2.1.0:
resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==}
retext-latin@4.0.0: retext-latin@4.0.0:
resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==} resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==}
@ -1562,6 +1666,10 @@ packages:
sisteransi@1.0.5: sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
slugify@1.6.9:
resolution: {integrity: sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg==}
engines: {node: '>=8.0.0'}
smartypants@0.2.2: smartypants@0.2.2:
resolution: {integrity: sha512-TzobUYoEft/xBtb2voRPryAUIvYguG0V7Tt3de79I1WfXgCwelqVsGuZSnu3GFGRZhXR90AeEYIM+icuB/S06Q==} resolution: {integrity: sha512-TzobUYoEft/xBtb2voRPryAUIvYguG0V7Tt3de79I1WfXgCwelqVsGuZSnu3GFGRZhXR90AeEYIM+icuB/S06Q==}
hasBin: true hasBin: true
@ -1583,10 +1691,18 @@ packages:
stringify-entities@4.0.4: stringify-entities@4.0.4:
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
supercluster@8.0.1:
resolution: {integrity: sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==}
supports-color@8.1.1: supports-color@8.1.1:
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
svelte-fa@4.0.4:
resolution: {integrity: sha512-85BomCGkTrH8kPDGvb8JrVwq9CqR9foprbKjxemP4Dtg3zPR7OXj5hD0xVYK0C+UCzFI1zooLoK/ndIX6aYXAw==}
peerDependencies:
svelte: ^4.0.0 || ^5.0.0
svelte2tsx@0.7.55: svelte2tsx@0.7.55:
resolution: {integrity: sha512-JWzgeM3lqySRNfqcsesvVEh8LhTWBxQJ9RMjzJ+VepdmXtVnNd0SbtGctG6+/fbHq0N6mhwSd823gszw9JHeGQ==} resolution: {integrity: sha512-JWzgeM3lqySRNfqcsesvVEh8LhTWBxQJ9RMjzJ+VepdmXtVnNd0SbtGctG6+/fbHq0N6mhwSd823gszw9JHeGQ==}
peerDependencies: peerDependencies:
@ -1625,6 +1741,9 @@ packages:
resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
tinyqueue@3.0.0:
resolution: {integrity: sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==}
trim-lines@3.0.1: trim-lines@3.0.1:
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
@ -2128,6 +2247,51 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
'@mapbox/jsonlint-lines-primitives@2.0.2': {}
'@mapbox/point-geometry@1.1.0': {}
'@mapbox/tiny-sdf@2.2.0': {}
'@mapbox/unitbezier@0.0.1': {}
'@mapbox/vector-tile@2.0.4':
dependencies:
'@mapbox/point-geometry': 1.1.0
'@types/geojson': 7946.0.16
pbf: 4.0.1
'@mapbox/whoots-js@3.1.0': {}
'@maplibre/geojson-vt@5.0.4': {}
'@maplibre/geojson-vt@6.1.0':
dependencies:
kdbush: 4.0.2
'@maplibre/maplibre-gl-style-spec@24.8.5':
dependencies:
'@mapbox/jsonlint-lines-primitives': 2.0.2
'@mapbox/unitbezier': 0.0.1
json-stringify-pretty-compact: 4.0.0
minimist: 1.2.8
quickselect: 3.0.0
tinyqueue: 3.0.0
'@maplibre/mlt@1.1.9':
dependencies:
'@mapbox/point-geometry': 1.1.0
'@maplibre/vt-pbf@4.3.0':
dependencies:
'@mapbox/point-geometry': 1.1.0
'@mapbox/vector-tile': 2.0.4
'@maplibre/geojson-vt': 5.0.4
'@types/geojson': 7946.0.16
'@types/supercluster': 7.1.3
pbf: 4.0.1
supercluster: 8.0.1
'@oslojs/encoding@1.1.0': {} '@oslojs/encoding@1.1.0': {}
'@parcel/watcher-android-arm64@2.5.6': '@parcel/watcher-android-arm64@2.5.6':
@ -2354,6 +2518,8 @@ snapshots:
'@types/estree@1.0.9': {} '@types/estree@1.0.9': {}
'@types/geojson@7946.0.16': {}
'@types/hast@3.0.4': '@types/hast@3.0.4':
dependencies: dependencies:
'@types/unist': 3.0.3 '@types/unist': 3.0.3
@ -2368,6 +2534,10 @@ snapshots:
dependencies: dependencies:
'@types/unist': 3.0.3 '@types/unist': 3.0.3
'@types/supercluster@7.1.3':
dependencies:
'@types/geojson': 7946.0.16
'@types/trusted-types@2.0.7': {} '@types/trusted-types@2.0.7': {}
'@types/unist@3.0.3': {} '@types/unist@3.0.3': {}
@ -2598,6 +2768,8 @@ snapshots:
dset@3.1.4: {} dset@3.1.4: {}
earcut@3.0.2: {}
entities@4.5.0: {} entities@4.5.0: {}
entities@6.0.1: {} entities@6.0.1: {}
@ -2663,6 +2835,8 @@ snapshots:
optionalDependencies: optionalDependencies:
picomatch: 4.0.4 picomatch: 4.0.4
fflate@0.8.2: {}
flattie@1.1.1: {} flattie@1.1.1: {}
fontace@0.4.1: fontace@0.4.1:
@ -2682,6 +2856,8 @@ snapshots:
github-slugger@2.0.0: {} github-slugger@2.0.0: {}
gl-matrix@3.4.4: {}
h3@1.15.11: h3@1.15.11:
dependencies: dependencies:
cookie-es: 1.2.3 cookie-es: 1.2.3
@ -2819,12 +2995,18 @@ snapshots:
dependencies: dependencies:
is-inside-container: 1.0.0 is-inside-container: 1.0.0
journalize@2.6.0: {}
js-yaml@4.1.1: js-yaml@4.1.1:
dependencies: dependencies:
argparse: 2.0.1 argparse: 2.0.1
json-stringify-pretty-compact@4.0.0: {}
jsonc-parser@3.3.1: {} jsonc-parser@3.3.1: {}
kdbush@4.0.2: {}
locate-character@3.0.0: {} locate-character@3.0.0: {}
longest-streak@3.1.0: {} longest-streak@3.1.0: {}
@ -2841,6 +3023,28 @@ snapshots:
'@babel/types': 7.29.0 '@babel/types': 7.29.0
source-map-js: 1.2.1 source-map-js: 1.2.1
maplibre-gl@5.24.0:
dependencies:
'@mapbox/jsonlint-lines-primitives': 2.0.2
'@mapbox/point-geometry': 1.1.0
'@mapbox/tiny-sdf': 2.2.0
'@mapbox/unitbezier': 0.0.1
'@mapbox/vector-tile': 2.0.4
'@mapbox/whoots-js': 3.1.0
'@maplibre/geojson-vt': 6.1.0
'@maplibre/maplibre-gl-style-spec': 24.8.5
'@maplibre/mlt': 1.1.9
'@maplibre/vt-pbf': 4.3.0
'@types/geojson': 7946.0.16
earcut: 3.0.2
gl-matrix: 3.4.4
kdbush: 4.0.2
murmurhash-js: 1.0.0
pbf: 4.0.1
potpack: 2.1.0
quickselect: 3.0.0
tinyqueue: 3.0.0
markdown-table@3.0.4: {} markdown-table@3.0.4: {}
marked-smartypants@1.1.12(marked@15.0.12): marked-smartypants@1.1.12(marked@15.0.12):
@ -3165,10 +3369,14 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
minimist@1.2.8: {}
mrmime@2.0.1: {} mrmime@2.0.1: {}
ms@2.1.3: {} ms@2.1.3: {}
murmurhash-js@1.0.0: {}
nanoid@3.3.12: {} nanoid@3.3.12: {}
neotraverse@0.6.18: {} neotraverse@0.6.18: {}
@ -3234,6 +3442,10 @@ snapshots:
dependencies: dependencies:
entities: 6.0.1 entities: 6.0.1
pbf@4.0.1:
dependencies:
resolve-protobuf-schema: 2.1.0
piccolore@0.1.3: {} piccolore@0.1.3: {}
picocolors@1.1.1: {} picocolors@1.1.1: {}
@ -3242,16 +3454,26 @@ snapshots:
picomatch@4.0.4: {} picomatch@4.0.4: {}
pmtiles@4.4.1:
dependencies:
fflate: 0.8.2
postcss@8.5.14: postcss@8.5.14:
dependencies: dependencies:
nanoid: 3.3.12 nanoid: 3.3.12
picocolors: 1.1.1 picocolors: 1.1.1
source-map-js: 1.2.1 source-map-js: 1.2.1
potpack@2.1.0: {}
prismjs@1.30.0: {} prismjs@1.30.0: {}
property-information@7.1.0: {} property-information@7.1.0: {}
protocol-buffers-schema@3.6.1: {}
quickselect@3.0.0: {}
radix3@1.1.2: {} radix3@1.1.2: {}
readdirp@4.1.2: readdirp@4.1.2:
@ -3337,6 +3559,10 @@ snapshots:
resolve-pkg-maps@1.0.0: {} resolve-pkg-maps@1.0.0: {}
resolve-protobuf-schema@2.1.0:
dependencies:
protocol-buffers-schema: 3.6.1
retext-latin@4.0.0: retext-latin@4.0.0:
dependencies: dependencies:
'@types/nlcst': 2.0.3 '@types/nlcst': 2.0.3
@ -3544,6 +3770,8 @@ snapshots:
sisteransi@1.0.5: {} sisteransi@1.0.5: {}
slugify@1.6.9: {}
smartypants@0.2.2: {} smartypants@0.2.2: {}
smol-toml@1.6.1: {} smol-toml@1.6.1: {}
@ -3559,10 +3787,18 @@ snapshots:
character-entities-html4: 2.1.0 character-entities-html4: 2.1.0
character-entities-legacy: 3.0.0 character-entities-legacy: 3.0.0
supercluster@8.0.1:
dependencies:
kdbush: 4.0.2
supports-color@8.1.1: supports-color@8.1.1:
dependencies: dependencies:
has-flag: 4.0.0 has-flag: 4.0.0
svelte-fa@4.0.4(svelte@5.55.5):
dependencies:
svelte: 5.55.5
svelte2tsx@0.7.55(svelte@5.55.5)(typescript@5.9.3): svelte2tsx@0.7.55(svelte@5.55.5)(typescript@5.9.3):
dependencies: dependencies:
dedent-js: 1.0.1 dedent-js: 1.0.1
@ -3618,6 +3854,8 @@ snapshots:
fdir: 6.5.0(picomatch@4.0.4) fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4 picomatch: 4.0.4
tinyqueue@3.0.0: {}
trim-lines@3.0.1: {} trim-lines@3.0.1: {}
trough@2.2.0: {} trough@2.2.0: {}

BIN
src/assets/Photo-4439.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 MiB

BIN
src/assets/Photo-5420.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 MiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,68 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import BeforeAfter from './BeforeAfter.svelte';
const { Story } = defineMeta({
title: 'Components/Multimedia/BeforeAfter',
component: BeforeAfter,
argTypes: {
handleColour: { control: 'color' },
width: {
control: 'select',
options: ['normal', 'wide', 'wider', 'widest', 'fluid'],
},
},
});
</script>
<script>
import beforeImg from './images/myrne-before.jpg';
import afterImg from './images/myrne-after.jpg';
</script>
<Story
name="Demo"
args={{
beforeSrc: beforeImg,
beforeAlt:
'Satellite image of Russian base at Myrne taken on July 7, 2020.',
afterSrc: afterImg,
afterAlt:
'Satellite image of Russian base at Myrne taken on Oct. 20, 2020.',
}}
/>
<Story name="With text" exportName="WithText">
<BeforeAfter
beforeSrc={beforeImg}
beforeAlt="Satellite image of Russian base at Myrne taken on July 7, 2020."
afterSrc={afterImg}
afterAlt="Satellite image of Russian base at Myrne taken on Oct. 20, 2020."
>
{#snippet beforeOverlay()}
<div class="overlay p-3 before">
<p class="h4 font-bold">July 7, 2020</p>
<p class="body-note">Initially, this site was far smaller.</p>
</div>
{/snippet}
{#snippet afterOverlay()}
<div class="overlay p-3 after">
<p class="h4 font-bold">Oct. 20, 2020</p>
<p class="body-note">But then forces built up.</p>
</div>
{/snippet}
{#snippet caption()}
<p class="body-note">Photos by MAXAR Technologies, 2021.</p>
{/snippet}
</BeforeAfter>
<style lang="scss">
.overlay {
background: rgba(0, 0, 0, 0.45);
max-width: 350px;
p {
color: #ffffff;
}
}
</style>
</Story>

View file

@ -0,0 +1,376 @@
<!-- @component `BeforeAfter` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-graphics-beforeafter--docs) -->
<script lang="ts">
import { type Snippet } from 'svelte';
import { throttle } from 'es-toolkit';
import Block from '../Block/Block.svelte';
import PaddingReset from '../PaddingReset/PaddingReset.svelte';
import type { ContainerWidth } from '../@types/global';
import { random4 } from '../../utils/';
interface Props {
/** Width of the chart within the text well. Options: wide, wider, widest, fluid */
width?: ContainerWidth;
/** Height of the component */
height?: number;
/**
* If set, makes the height a ratio of the component's width.
*/
heightRatio?: number;
/**
* Before image source
*/
beforeSrc: string;
/**
* Before image altText
*/
beforeAlt: string;
/**
* After image source
*/
afterSrc: string;
/**
* After image altText
*/
afterAlt: string;
/**
* Class to target with SCSS.
*/
class?: string;
/** Drag handle colour */
handleColour?: string;
/** Drag handle opacity */
handleInactiveOpacity?: number;
/** Margin at the edge of the image to stop dragging */
handleMargin?: number;
/** Percentage of the component width the handle will travel ona key press */
keyPressStep?: number;
/** Initial offset of the handle, between 0 and 1. */
offset?: number;
/** ID to target with SCSS. */
id?: string;
/**
* Optional snippet for a custom overlay for the before image.
*/
beforeOverlay?: Snippet;
/**
* Optional snippet for a custom overlay for the after image.
*/
afterOverlay?: Snippet;
/**
* Optional snippet for a custom caption.
*/
caption?: Snippet;
/** Custom ARIA label language to label the component. */
ariaLabel?: string;
}
let {
width = 'normal',
height = 600,
heightRatio,
beforeSrc,
beforeAlt,
afterSrc,
afterAlt,
class: cls = '',
handleColour = 'white',
handleInactiveOpacity = 0.9,
handleMargin = 20,
keyPressStep = 0.05,
offset = 0.5,
id = 'before-after-' + random4() + random4(),
beforeOverlay,
afterOverlay,
caption,
ariaLabel = 'Stacked before and after images with an adjustable slider',
}: Props = $props();
/** DOM nodes are undefined until the component is mounted — in other words, you should read it inside an effect or an event handler, but not during component initialisation.
*/
let img: HTMLImageElement | undefined = $state(undefined);
/** Defaults with an empty DOMRect with all values set to 0 */
let imgOffset: DOMRect = $state(new DOMRect());
let sliding = false;
let figure: HTMLElement | undefined = $state(undefined);
let beforeOverlayWidth = $state(0);
let isFocused = false;
let containerWidth: number = $state(0); // Defaults to 0
let containerHeight = $derived(
containerWidth && heightRatio ? containerWidth * heightRatio : height
);
let w = $derived(imgOffset.width);
let x = $derived(w * offset);
let figStyle = $derived(`width:100%;height:${containerHeight}px;`);
const imgStyle = 'width:100%;height:100%;';
let beforeOverlayClip = $derived(
x < beforeOverlayWidth ? Math.abs(x - beforeOverlayWidth) : 0
);
/** Toggle `isFocused` */
const onfocus = () => (isFocused = true);
const onblur = () => (isFocused = false);
/** Handle left or right arrows being pressed */
const handleKeyDown = (e: KeyboardEvent) => {
if (!isFocused) return;
const { code, key } = e;
const margin = handleMargin / w;
if (code === 'ArrowRight' || key === 'ArrowRight') {
offset = Math.min(1 - margin, offset + keyPressStep);
} else if (code === 'ArrowLeft' || key === 'ArrowLeft') {
offset = Math.max(0 + margin, offset - keyPressStep);
}
};
/** Measure image and set image offset */
const measureImage = () => {
if (img && img.complete) imgOffset = img.getBoundingClientRect();
};
/** Reset image offset on resize */
const resize = () => {
measureImage();
};
/** Measure image and set image offset on load */
const measureLoadedImage = (e: Event) => {
if (e.type === 'load') {
imgOffset = (e.target as HTMLImageElement).getBoundingClientRect();
}
};
/** Move the slider */
const move = (e: MouseEvent | TouchEvent) => {
if (sliding && imgOffset) {
const el =
e instanceof TouchEvent && e.touches ? e.touches[0] : (e as MouseEvent);
const figureOffset =
figure ?
parseInt(window.getComputedStyle(figure).marginLeft.slice(0, -2))
: 0;
let x = el.pageX - figureOffset - imgOffset.left;
x =
x < handleMargin ? handleMargin
: x > w - handleMargin ? w - handleMargin
: x;
offset = x / w;
}
};
/** Starts the slider */
const start = (e: MouseEvent | TouchEvent) => {
sliding = true;
move(e);
};
/** Sets `sliding` to `false`*/
const end = () => {
sliding = false;
};
/** Keep this warning since these values are often read from an ArchieML doc, which will not trigger typescript errors even if required values don't exist */
if (!(beforeSrc && beforeAlt && afterSrc && afterAlt)) {
console.warn('Missing required src or alt props for BeforeAfter component');
}
/** @TODO - Double check if this onMount is still necessary */
// onMount(() => {
// // This is necessary b/c on:load doesn't reliably fire on the image...
// const interval = setInterval(() => {
// if (imgOffset) clearInterval(interval);
// if (img && img.complete && !imgOffset) measureImage();
// }, 50);
// });
</script>
<svelte:window
ontouchmove={move}
ontouchend={end}
onmousemove={move}
onmouseup={end}
onresize={throttle(resize, 100)}
onkeydown={handleKeyDown}
/>
<!-- Since we usually read these values from ArchieML, check that they exist -->
{#if beforeSrc && beforeAlt && afterSrc && afterAlt}
<Block {width} {id} class="photo before-after fmy-6 {cls}">
<div style="height: {containerHeight}px;" bind:clientWidth={containerWidth}>
<button
style={figStyle}
class="before-after-container relative overflow-hidden my-0 mx-auto"
ontouchstart={start}
onmousedown={start}
bind:this={figure}
aria-label={ariaLabel}
>
<img
bind:this={img}
src={afterSrc}
alt={afterAlt}
onload={measureLoadedImage}
style={imgStyle}
class="after absolute block m-0 max-w-full object-cover"
aria-describedby={beforeOverlay ?
`${id}-before-description`
: undefined}
/>
<img
src={beforeSrc}
alt={beforeAlt}
style="clip: rect(0 {x}px {containerHeight}px 0);{imgStyle}"
class="before absolute block m-0 max-w-full object-cover"
aria-describedby={afterOverlay ?
`${id}-after-description`
: undefined}
/>
{#if beforeOverlay}
<div
id="{id}-before-description"
class="overlay-container before absolute"
bind:clientWidth={beforeOverlayWidth}
style="clip-path: inset(0 {beforeOverlayClip}px 0 0);"
>
<!-- Overlay for before image -->
{@render beforeOverlay()}
</div>
{/if}
{#if afterOverlay}
<div
id="{id}-after-description"
class="overlay-container after absolute"
>
<!-- Overlay for after image -->
{@render afterOverlay()}
</div>
{/if}
<div
tabindex="0"
role="slider"
aria-valuenow={Math.round(offset * 100)}
class="handle"
style="left: calc({offset *
100}% - 20px); --before-after-handle-colour: {handleColour}; --before-after-handle-inactive-opacity: {handleInactiveOpacity};"
{onfocus}
{onblur}
>
<div class="arrow-left"></div>
<div class="arrow-right"></div>
</div>
</button>
</div>
{#if caption}
<PaddingReset containerIsFluid={width === 'fluid'}>
<aside class="before-after-caption mx-auto" id={`${id}-caption`}>
<!-- Caption for image credits -->
{@render caption()}
</aside>
</PaddingReset>
{/if}
</Block>
{/if}
<style lang="scss">
@use 'mixins' as mixins;
button.before-after-container {
box-sizing: content-box;
text-align: inherit;
img {
top: 0;
left: 0;
z-index: 20;
&.after {
z-index: 21;
}
&.before {
z-index: 22;
}
user-select: none;
}
.overlay-container {
top: 0;
:global(:first-child) {
margin-block-start: 0;
}
:global(:last-child) {
margin-block-end: 0;
}
&.before {
left: 0;
z-index: 23;
}
&.after {
right: 0;
z-index: 21;
}
}
}
.handle {
z-index: 30;
width: 40px;
height: 40px;
cursor: move;
background: none;
user-select: none;
position: absolute;
border-radius: 50px;
top: calc(50% - 20px);
border: 4px solid var(--before-after-handle-colour);
opacity: var(--before-after-handle-inactive-opacity, 0.6);
box-shadow: 1px 1px 3px #333;
&:hover,
&:active,
&:focus {
opacity: 1;
}
&:before,
&:after {
content: '';
box-shadow: 0 0 3px #333;
height: 9999px;
position: absolute;
left: calc(50% - 2px);
border: 2px solid var(--before-after-handle-colour);
}
&:before {
top: 40px;
}
&:after {
bottom: 40px;
}
.arrow-right,
.arrow-left {
width: 0;
height: 0;
user-select: none;
position: relative;
border-block-start: 10px solid transparent;
border-block-end: 10px solid transparent;
}
.arrow-right {
inset-inline-start: 19px;
inset-block-end: 14px;
border-inline-start: 10px solid var(--before-after-handle-colour);
}
.arrow-left {
inset-inline-start: 3px;
inset-block-start: 6px;
border-inline-end: 10px solid var(--before-after-handle-colour);
}
}
.before-after-caption {
:global(p) {
@include mixins.body-caption;
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 KiB

View file

@ -42,7 +42,7 @@
</div> </div>
<style lang="scss"> <style lang="scss">
@use '../../scss/mixins' as mixins; @use 'mixins' as mixins;
.article-block { .article-block {
max-width: var(--normal-column-width, 660px); max-width: var(--normal-column-width, 660px);

View file

@ -0,0 +1,170 @@
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';
let { content }: Props = $props();
</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} />
## Translated byline and datelines
Use [snippets](https://svelte.dev/docs/svelte/snippet) to conditionally customise the byline, published and updated datelines for different languages.
```svelte
<!-- In App.svelte -->
<script>
import {
Byline,
getAuthorPageUrl,
formatTime,
} from '@reuters-graphics/graphics-components';
let { content }: Props = $props();
// Note: In graphics kit, `locale` is already defined in `App.svelte`
</script>
```
```svelte
<!-- In App.svelte -->
<!-- Define custom translation snippets for different languages above the <Byline/> component -->
{#snippet esByline()}
Por
{#each content.authors as author, i}
<a
class="no-underline whitespace-nowrap text-primary font-bold"
href={getAuthorPageUrl(author)}
rel="author"
>
{author.trim()}</a
>{#if content.authors.length > 1 && i < content.authors.length - 2},{/if}
{#if content.authors.length > 1 && i === content.authors.length - 2}y&nbsp;{/if}
{/each}
{/snippet}
{#snippet esPublished()}
Publicado <time datetime="2026-04-08T10:00:00.000Z">
{new Date(content.publishTime).toLocaleDateString('es-ES', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}&nbsp;&nbsp;{formatTime(content.publishTime)}</time
>
{/snippet}
```
```svelte
<!-- In App.svelte -->
<!-- Conditionally render custom translation snippets depending on the locale -->
<Byline
authors={content.authors}
publishTime={content.publishTime}
byline={locale === 'es' ? esByline : undefined}
published={locale === 'es' ? esPublished : undefined}
/>
```
<Canvas of={BylineStories.Translation} />
## Custom author page
By default, the `Byline` component will hyperlink each author's byline to their Reuters.com page, formatted `https://www.reuters.com/authors/{author-slug}/`.
To hyperlink to different pages or email addresses, pass a custom function to the `getAuthorPage` prop.
```svelte
<!-- Pass a custom function as `getAuthorPage` -->
<Byline
authors={[
'Dea Bankova',
'Prasanta Kumar Dutta',
'Anurag Rao',
'Mariano Zafra',
]}
publishTime="2021-09-12T00:00:00Z"
updateTime="2021-09-12T13:57:00Z"
getAuthorPage={(author) => {
return `mailto:${author.replace(' ', '')}@example.com`;
}}
/>
```
<Canvas of={BylineStories.CustomAuthorPage} />
````

View file

@ -0,0 +1,108 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import { formatTime, getAuthorPageUrl } from '../../utils/index.js';
import Byline from './Byline.svelte';
const { Story } = defineMeta({
title: 'Components/Text elements/Byline',
component: Byline,
tags: ['autodocs'],
argTypes: {
align: {
control: 'select',
options: ['auto', 'center'],
},
},
});
let authors = [
'Dea Bankova',
'Prasanta Kumar Dutta',
'Anurag Rao',
'Mariano Zafra',
];
let publishTime = '2021-09-12T00:00:00Z';
let locale = 'es';
</script>
<Story
name="Demo"
args={{
authors,
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>
<!-- Translation snippets -->
{#snippet esByline()}
Por
{#each authors as author, i}
<a
class="no-underline whitespace-nowrap text-primary font-bold"
href={getAuthorPageUrl(author)}
rel="author"
>
{author.trim()}</a
>{#if authors.length > 1 && i < authors.length - 2},{/if}
{#if authors.length > 1 && i === authors.length - 2}y&nbsp;{/if}
{/each}
{/snippet}
{#snippet esPublished()}
Publicado <time datetime="2026-04-08T10:00:00.000Z">
{new Date(publishTime).toLocaleDateString('es-ES', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}&nbsp;&nbsp;{formatTime(publishTime)}</time
>
{/snippet}
<Story name="Translation" tags={['!autodocs', '!dev']}>
In locale = `es`:
<Byline
publishTime="2021-09-12T00:00:00Z"
{authors}
byline={locale === 'es' ? esByline : undefined}
published={locale === 'es' ? esPublished : undefined}
></Byline>
In locale = `en`:
<Byline
publishTime="2021-09-12T00:00:00Z"
{authors}
byline={undefined}
published={undefined}
/>
</Story>
<Story
name="Custom author page"
exportName="CustomAuthorPage"
tags={['!autodocs', '!dev']}
args={{
authors,
publishTime: '2021-09-12T00:00:00Z',
updateTime: '2021-09-12T13:57:00Z',
getAuthorPage: (author: string) => {
return `mailto:${author.replace(' ', '')}@example.com`;
},
}}
/>

View file

@ -0,0 +1,181 @@
<!-- @component `Byline` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-text-elements-byline--docs) -->
<script lang="ts">
import { getAuthorPageUrl, formatTime } 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 areSameDay = (first: Date, second: Date) =>
first.getFullYear() === second.getFullYear() &&
first.getMonth() === second.getMonth() &&
first.getDate() === second.getDate();
</script>
<Block {id} class="byline-container {alignmentClass} {cls}" width="normal">
<aside class="article-metadata font-subhed">
<div class="byline body-caption fmb-1">
{#if byline}
<!-- Custom byline -->
{@render byline()}
{:else}
By
{#if authors.length > 0}
{#each authors as author, i}
<a
class="no-underline whitespace-nowrap text-primary font-bold"
href={getAuthorPage(author)}
rel="author"
>
{author.trim()}</a
>{#if authors.length > 1 && i < authors.length - 2},{/if}
{#if authors.length > 1 && i === authors.length - 2}and&nbsp;{/if}
{/each}
{:else}
<a
href="https://www.reuters.com"
class="no-underline whitespace-nowrap text-primary font-bold"
>Reuters</a
>
{/if}
{/if}
</div>
<div class="dateline body-caption fmt-0">
{#if published}
<div class="whitespace-nowrap inline-block">
<!-- Custom published dateline snippet -->
<time datetime={publishTime}>
{@render published()}
</time>
</div>
{:else if isValidDate(publishTime)}
<div class="whitespace-nowrap inline-block">
Published
<time datetime={publishTime}>
{#if updateTime && isValidDate(updateTime)}
{apdate(new Date(publishTime))}
{:else}
{apdate(new Date(publishTime))}&nbsp;&nbsp;{formatTime(
publishTime
)}
{/if}
</time>
</div>
{/if}
{#if updated}
<div class="whitespace-nowrap inline-block">
<!-- Custom updated dateline snippet -->
<time datetime={updateTime}>
{@render updated()}
</time>
</div>
{:else if isValidDate(publishTime) && isValidDate(updateTime || '')}
<div class="whitespace-nowrap inline-block">
Last updated
<time datetime={updateTime}>
{#if areSameDay(new Date(publishTime), new Date(updateTime || new Date()))}
{formatTime(updateTime || '')}
{:else}
{apdate(
new Date(updateTime || new Date())
)}&nbsp;&nbsp;{formatTime(updateTime || '')}
{/if}
</time>
</div>
{/if}
</div>
</aside>
</Block>
<style lang="scss">
@use 'mixins' as *;
.byline {
a {
&:hover {
text-decoration-line: underline;
}
}
}
@media (min-width: $column-width-narrower) {
.dateline {
div {
&:not(:last-child) {
&:after {
content: '·';
margin: 0 2px 0 5px;
}
}
}
}
}
</style>

View file

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

View file

@ -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} />

View file

@ -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" />

View file

@ -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 '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>

View file

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

View file

@ -0,0 +1,41 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import DatawrapperChart from './DatawrapperChart.svelte';
const { Story } = defineMeta({
title: 'Components/Graphics/DatawrapperChart',
component: DatawrapperChart,
tags: ['autodocs'],
argTypes: {
width: {
control: 'select',
options: ['normal', 'wide', 'wider', 'widest', 'fluid'],
},
},
});
</script>
<Story
name="Demo"
args={{
src: 'https://reuters.com/graphics/USA-ABORTION/lgpdwggnwvo/media-embed.html',
id: 'abortion-rights-map',
ariaLabel: 'map',
frameTitle: 'Global abortion access',
}}
/>
<Story
name="With chatter"
tags={['!autodocs']}
args={{
frameTitle: 'Global abortion access',
ariaLabel: 'map',
id: 'abortion-rights-map',
src: 'https://reuters.com/graphics/USA-ABORTION/lgvdwemlbpo/media-embed.html',
title: 'Global abortion access',
description: 'A map of worldwide access to abortion.',
notes:
'Note: Different indicators and additional restrictions, including different gestational limits, apply in some countries. Refer to source for full classification. Current as of May 4, 2022.\n\nSource: Center for Reproductive Rights',
}}
/>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,29 @@
<script module lang="ts">
import EndNotes from './EndNotes.svelte';
import { defineMeta } from '@storybook/addon-svelte-csf';
const { Story } = defineMeta({
title: 'Components/Text elements/EndNotes',
component: EndNotes,
});
</script>
<script>
const notes = [
{
title: 'Note',
text: 'Data is current as of today.',
},
{
title: 'Sources',
text: 'Data, Inc.',
},
{
title: 'Edited by',
text: '<a href="https://www.reuters.com/graphics/">Editor</a>, Copyeditor',
},
];
</script>
<Story name="Demo" args={{ notes }} />

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

View file

@ -21,7 +21,7 @@
</form> </form>
<style lang="scss"> <style lang="scss">
@use '../../../scss/mixins' as mixins; @use 'mixins' as mixins;
label { label {
margin-bottom: 0.25rem; margin-bottom: 0.25rem;

View file

@ -149,7 +149,7 @@
{/if} {/if}
<style lang="scss"> <style lang="scss">
@use '../../scss/mixins' as mixins; @use 'mixins' as mixins;
header { header {
text-align: center; text-align: center;

View file

@ -283,7 +283,7 @@
</div> </div>
<style lang="scss"> <style lang="scss">
@use '../../../scss/mixins' as mixins; @use 'mixins' as mixins;
[data-svelte-typeahead] { [data-svelte-typeahead] {
position: relative; position: relative;

View file

@ -0,0 +1,23 @@
import { Meta } from '@storybook/blocks';
import * as UtilFunctionStories from './Utils.stories.svelte';
<Meta of={UtilFunctionStories} />
# Util functions
This library provides utility functions that can be used across various components and applications.
## Prettify date in the Reuters format
The function `prettifyDate` formats the input string, which is expected to be in English, to format the month and time designator (AM/PM) according to the Reuters style guide. The function is case agnostic and will format both full month names and their 3-letter abbreviations (i.e. `Mar` or `Jun`) correctly.
```javascript
import { prettifyDate } from '@reuters-graphics/graphics-components';
// Example usage
prettifyDate('January 1, 2023, 10:00 AM'); // returns 'Jan. 1, 2023, 10:00 a.m.'
prettifyDate('Jan 1, 2023, 10:00 PM'); // returns 'Jan. 1, 2023, 10:00 p.m.'
prettifyDate('MAR. 2025'); // returns 'March 2025'
prettifyDate('sep. 1, 2023, 10:00PM'); // returns 'Sept. 1, 2023, 10:00 p.m.'
```

View file

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

View file

@ -192,7 +192,7 @@
</div> </div>
<style lang="scss"> <style lang="scss">
@use '../../scss/mixins' as mixins; @use 'mixins' as mixins;
.geocoder-input-wrapper { .geocoder-input-wrapper {
position: relative; position: relative;

View file

@ -128,7 +128,7 @@
</div> </div>
<style lang="scss"> <style lang="scss">
@use '../../scss/mixins' as mixins; @use 'mixins' as mixins;
div.container { div.container {
display: contents; display: contents;

View file

@ -145,7 +145,7 @@
</div> </div>
<style lang="scss"> <style lang="scss">
@use '../../scss/mixins' as mixins; @use 'mixins' as mixins;
.headline-wrapper { .headline-wrapper {
:global(.dek) { :global(.dek) {
max-width: mixins.$column-width-normal; max-width: mixins.$column-width-normal;

View file

@ -0,0 +1,37 @@
import { Meta, Canvas } from '@storybook/blocks';
import * as HeadpileStories from './Headpile.stories.svelte';
<Meta of={HeadpileStories} />
# Headpile
The `Headpile` component is a headshot-bulleted list of people, identifying them with their names, roles and a short description of their significance to a story.
It's designed to be used with headshots that have had their background removed, which can be done in the [Preview app](https://support.apple.com/en-gb/guide/preview/prvw15636/mac?#apd320b3b1b750a4) on macOS.
```svelte
<script>
import { Headpile } from '@reuters-graphics/graphics-components';
import { assets } from '$app/paths'; // 👈 If using in the graphics kit...
</script>
<Headpile
figures={[
{
img: `${assets}/images/person-A.jpg`,
name: 'General Abdel Fattah al-Burhan',
role: "Sudan's Sovereign Council Chief and military commander",
text: 'Burhan was little known in public life until taking part in the coup ...',
},
{
img: `${assets}/images/person-B.jpg`,
name: 'General Mohamed Hamdan Dagalo',
role: 'Leader of the Sudanese paramilitary Rapid Support Forces (RSF)',
text: 'Popularly known as Hemedti, Dagalo rose from lowly beginnings ...',
},
]}
/>
```
<Canvas of={HeadpileStories.Demo} />

View file

@ -0,0 +1,37 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import Headpile from './Headpile.svelte';
import hed1 from './images/abdel.png';
import hed2 from './images/hemedti.png';
const { Story } = defineMeta({
title: 'Components/Text elements/Headpile',
component: Headpile,
argTypes: {},
});
const defaultArgs = [
{
name: 'General Abdel Fattah al-Burhan',
role: "Sudan's Sovereign Council Chief and military commander",
img: hed1,
text: 'Burhan was little known in public life until taking part in the coup against Bashir in 2019 after a popular uprising against his rule. In August 2019, his role as de facto head of state was affirmed when he became head of the Sovereign Council, a body comprising civilian and military leaders formed to oversee the transition towards elections.',
// colour: '#957caa',
},
{
name: 'General Mohamed Hamdan Dagalo',
role: 'Leader of the Sudanese paramilitary Rapid Support Forces (RSF)',
img: hed2,
text: "Popularly known as Hemedti, Dagalo rose from lowly beginnings as a camel trader to head a widely feared Arab militia that crushed a revolt in Darfur, winning him influence and eventually a role as Sudan's former deputy head of state.\r\n\r\nOver the past decade, he has been a key figure in Sudanese politics, aiding in the ousting of Bashir in 2019 and suppressing pro-democracy protests. As the country limped from one economic crisis to another, Hemedti became one of Sudans richest men, exporting gold from mines in Darfur seized by his fighters.",
colour: '#afb776',
},
];
</script>
<Story
name="Demo"
args={{
figures: defaultArgs,
}}
/>

View file

@ -0,0 +1,81 @@
<script lang="ts">
import type { ContainerWidth } from '../@types/global';
import Block from '../Block/Block.svelte';
import KeyFigure from './KeyFigure.svelte';
interface Props {
/**
* Add classes to target with custom CSS.
*/
class: string;
/**
* Add an id to target with custom CSS.
*/
id: string;
/**
* Width of the container.
*/
width: Extract<ContainerWidth, 'normal' | 'wide'>;
/**
* Default background colour to be used as a mount behind the headshot.
*/
colour: string;
/**
* Individual figures -- i.e., people -- for the headpile.
*/
figures: {
/**
* Headshot image src. Be sure to prefix the image
*
* ```typescript
* import { assets } from '$app/paths';
*
* const imgSrc = `${assets}/images/my-image.jpg`;
* ```
*/
img: string;
/**
* Figure name.
*/
name: string;
/**
* Figure role or title.
*/
role?: string;
/**
* Text describing the person.
*/
text: string;
/**
* Background colour to be used as a mount behind the headshot.
*/
colour?: string;
}[];
}
let {
figures,
class: cls,
id,
width = 'normal',
colour = '#cccccc',
}: Props = $props();
</script>
<Block class="fmy-6 {cls} {id} {width}">
<div class="figures">
{#each figures as figure}
<KeyFigure {...{ ...figure, colour: figure.colour ?? colour }} />
{/each}
</div>
</Block>
<style lang="scss">
@use 'mixins' as mixins;
div.figures {
display: flex;
flex-direction: column;
gap: 2.75rem;
}
</style>

View file

@ -0,0 +1,41 @@
<script lang="ts">
let { img = '', colour = 'var(--theme-colour-accent)' } = $props();
</script>
<div class="headshot-wrapper">
<div class="background" style="background-color: {colour};"></div>
<div class="headshot" style="background-image: url({img}); "></div>
</div>
<style lang="scss">
.headshot-wrapper {
width: 7rem;
height: 6.75rem;
position: relative;
margin-block-start: -1.75rem;
margin-block-end: -1.75rem;
border-radius: 0.25rem;
overflow: hidden;
}
.background {
position: absolute;
inset-block-end: 0;
inset-inline-start: 0;
width: 7rem;
height: 4.75rem;
display: inline-block;
border-radius: 0.25rem;
}
.headshot {
display: inline-block;
width: 100%;
height: 100%;
background-size: 106%;
background-position: center bottom;
background-repeat: no-repeat;
position: absolute;
inset-block-end: 0;
inset-inline-start: 0;
overflow: hidden;
}
</style>

View file

@ -0,0 +1,108 @@
<script lang="ts">
import { Markdown } from '@reuters-graphics/svelte-markdown';
import Headshot from './Headshot.svelte';
import { MediaQuery } from 'svelte/reactivity';
interface Props {
img: string;
name: string;
role?: string;
text: string;
colour?: string;
}
let { name, role, img, text, colour }: Props = $props();
const mobile = new MediaQuery('max-width: 600px');
</script>
<div>
<div class="wrapper-profile">
<div>
<Headshot {img} {colour} />
</div>
<div class="text">
<div class="title">{name}</div>
<div class="role">
{role || ''}
</div>
{#if !mobile.current}
<div class="description desktop">
<Markdown source={text} />
</div>
{/if}
</div>
</div>
{#if mobile.current}
<div class="description mobile">
<Markdown source={text} />
</div>
{/if}
</div>
<style lang="scss">
@use 'mixins' as mixins;
.wrapper-profile {
display: flex;
align-items: flex-start;
justify-content: start;
gap: 1rem;
width: 100%;
min-height: 5.5rem;
}
.title {
@include mixins.font-bold;
@include mixins.text-base;
@include mixins.leading-none;
@media (max-width: 450px) {
font-size: calc(0.9 * var(--theme-font-size-base, 1rem));
}
}
.role {
border-block-start: 0.5px solid var(--tr-muted-grey);
margin-inline-start: -0.75rem;
padding-inline-start: 0.75rem;
margin-block-start: 0.25rem;
padding-block-start: 0.25rem;
@include mixins.font-note;
@include mixins.text-secondary;
@include mixins.text-sm;
@include mixins.font-light;
@include mixins.leading-tighter;
@include mixins.fmb-4;
@media (max-width: 450px) {
@include mixins.text-xs;
}
}
.description {
:global(p) {
@include mixins.font-note;
font-size: calc(0.9 * var(--theme-font-size-base, 1rem));
font-weight: 300;
@include mixins.fmb-0;
text-wrap: pretty;
}
&.desktop {
display: block;
}
&.mobile {
display: none;
}
@media (max-width: 600px) {
&.desktop {
display: none;
}
&.mobile {
display: block;
}
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 KiB

View file

@ -0,0 +1,329 @@
import { Meta, Canvas } from '@storybook/blocks';
import * as HeroHeadlineStories from './HeroHeadline.stories.svelte';
<Meta of={HeroHeadlineStories} />
# HeroHeadline
The `HeroHeadline` component creates a Reuters Graphics headline with a hero media, which can be a graphic, photo, video or other media.
By default, the hero is in the background, i.e., the headline and dek are stacked on top of the hero. You can unstack and insert the hero media inline -- i.e., before or after the headline -- by setting `stacked: false`. [Read more.](?/iframe.html?viewMode=docs&id=components-text-elements-heroheadline--docs&globals=&args=#inline-hero)
## Photo hero
To use a photo as the hero, simply pass the image source to the `img` prop.
```svelte
<!-- App.svelte -->
<script>
import { HeroHeadline } from '@reuters-graphics/graphics-components';
import { assets } from '$app/paths'; // 👈 If using in the graphics kit...
let { embedded = false } = $props(); // 👈 If using in the graphics kit...
</script>
<HeroHeadline
{embedded}
img={`${assets}/images/polar-bear.jpg`}
ariaDescription="A photo of a polar bear"
notes="Photo by REUTERS"
section={'World News'}
hed={'Reuters Graphics Interactive'}
dek={'The beginning of a beautiful page'}
authors={['Jane Doe', 'John Doe']}
/>
```
<Canvas of={HeroHeadlineStories.PhotoHero} />
## Transparent site header
In the graphics kit, set styles in `global.scss` to make the Reuters site header transparent and make the hero go all the way to the top of the page:
```scss
// global.scss
.nav-container {
background-color: transparent !important;
}
.nav-container .inner {
background-color: transparent !important;
border: none !important;
}
.hero-wrapper {
margin-block-start: -64px;
}
```
<Canvas of={HeroHeadlineStories.TransparentHeader} />
## Ai2svelte hero
To use an ai2svelte graphic as the hero, wrap your ai2svelte component in a `GraphicBlock` component and insert it inside `HeroHeadline`.
To customise styles, use CSS to target the class passed to `HeroHeadline`.
> **Note:** Pass `notes` and `ariaDescription` to the `GraphicBlock` component to provide additional information about the ai2svelte graphic.
```svelte
<!-- App.svelte -->
<script>
import {
HeroHeadline,
GraphicBlock,
} from '@reuters-graphics/graphics-components';
import QuakeMap from './ai2svelte/graphic.svelte'; // Your ai2svelte component
import { assets } from '$app/paths'; // 👈 If using in the graphics kit...
let { embedded = false } = $props(); // 👈 If using in the graphics kit...
</script>
<HeroHeadline
{embedded}
hed="Earthquake devastates Afghanistan"
hedSize="big"
hedWidth="wide"
class="custom-hero mb-0"
authors={[
'Anand Katakam',
'Vijdan Mohammad Kawoosa',
'Adolfo Arranz',
'Wen Foo',
'Simon Scarr',
'Aman Bhargava',
'Jitesh Chowdhury',
'Manas Sharma',
'Aditi Bhandari',
]}
publishTime={new Date('2022-06-24').toISOString()}
>
<GraphicBlock
width="widest"
role="figure"
class="my-0"
ariaDescription="Earthquake impact map"
>
<!-- Pass `assetsPath` if in graphics kit -->
<QuakeMap assetsPath={assets || '/'} />
</GraphicBlock>
</HeroHeadline>
```
Add styles in `global.scss`:
```scss
// global.scss
// Customise styles using the class (e.g. `custom-hero` here) passed to `HeroHeadline`
.hero-wrapper {
.custom-hero.headline {
// Adjust vertical positioning
align-items: flex-end !important;
@media (max-width: 1100px) {
// Adjust line length of title
max-width: var(--normal-column-width) !important;
}
}
// Make hero shorter than 100vh
--heroHeight: 85svh;
@media (max-width: 960px) {
--heroHeight: 65svh;
}
// For small height
@media (max-height: 850px) {
--heroHeight: 100svh;
}
// Custom hero sizing for landscape mobile
@media (max-width: 960px) and (orientation: landscape) {
--heroHeight: 200svh;
}
}
// Override default fixed height for hero layout in embeds
.hero-wrapper.embedded {
--heroHeight: 1000px;
}
```
<Canvas of={HeroHeadlineStories.Ai2svelteHero} />
## Video hero
To add a video as the hero, use the [Video](?path=/docs/components-multimedia-video--docs) component. To customise styles, use CSS to target the class passed to `HeroHeadline`.
> **Note:** Pass `notes` and `ariaDescription` to the `GraphicBlock` component to provide additional information about the video.
```svelte
<script>
import { HeroHeadline, Video } from '@reuters-graphics/graphics-components';
import { assets } from '$app/paths'; // 👈 If using in the graphics kit...
let { embedded = false } = $props(); // 👈 If using in the graphics kit...
</script>
<HeroHeadline
{embedded}
class="video-hero"
hed="The conflict in Ethiopia"
hedSize="bigger"
hedWidth="wide"
authors={['Aditi Bhandari ', 'David Lewis']}
publishTime={new Date('2020-12-18').toISOString()}
>
<Video
width="widest"
class="my-0"
showControls={false}
preloadVideo="auto"
playVideoWhenInView={false}
src={`${assets}/videos/intro.mp4`}
poster={`${assets}/images/video-poster-intro.jpg`}
notes="Drone footage from the Village 8 refugee camp in Sudan."
ariaDescription="Aerial footage of people houses in refugee camp"
/>
</HeroHeadline>
```
Add styles in `global.scss`:
```scss
// global.scss
// Customise styles using the class (e.g. `video-hero` here) passed to `HeroHeadline`
.hero-wrapper {
--heroHeight: calc(100svh - 60px);
.video-hero.headline {
header {
// Adjust vertical position as offset from default center
top: calc(50svh - 250px);
}
h1 {
color: #ffd430;
text-shadow: 3px 4px 7px rgba(81, 67, 21, 0.8);
}
}
}
```
<Canvas of={HeroHeadlineStories.VideoHero} />
## Inline hero
To use a photo, graphic, video, etc. as an inline hero -- i.e., to make the hero appear _after_ the headline and dek, instead of stacked underneath -- set `stacked` to `false`. Otherwise, add your hero media in the same way as documented above.
```svelte
<!-- App.svelte -->
<script>
import {
HeroHeadline,
GraphicBlock,
} from '@reuters-graphics/graphics-components';
import { assets } from '$app/paths'; // 👈 If using in the graphics kit...
let { embedded = false } = $props(); // 👈 If using in the graphics kit...
</script>
<!-- Set `stacked` to `false` -->
<HeroHeadline
{embedded}
stacked={false}
section="Global news"
hed="The plunge from 29,000 feet"
dek="How China Eastern Airlines flight MU5735 went from an uneventful flight at cruising altitude to disaster in just minutes."
class="mb-0"
authors={['Simon Scarr', 'Vijdan Mohammad Kawoosa']}
publishTime={new Date('2020-01-01').toISOString()}
>
<GraphicBlock
width="widest"
role="figure"
class="my-0"
ariaDescription="Earthquake impact map"
notes="Source: Satellite image from Google, Maxar Technologies, CNES/Airbus, Landsat/Copernicus"
>
<!-- Pass `assetsPath` if in graphics kit -->
<CrashMap assetsPath={assets || '/'} />
</GraphicBlock>
</HeroHeadline>
```
<Canvas of={HeroHeadlineStories.InlineHero} />
## Custom hed, dek and byline
The `HeroHeadline` component internally uses the [Headline](?path=/docs/components-text-elements-headline--docs) component to render the headline and dek, which lets you to customise the headline and dek by passing [snippets](https://svelte.dev/docs/svelte/snippet) into the `hed` and `dek` props.
Since `Headline` internally uses the [Byline](?path=/docs/components-text-elements-headline--docs) component, you can also customise the author page hyperlink and bylines with the `getAuthorPage`, `byline`, `published` and `updated` props.
```svelte
<!-- App.svelte -->
<HeroHeadline
class="custom-hed"
authors={[
'Prasanta Kumar Dutta',
'Dea Bankova',
'Aditi Bhandari',
'Anurag Rao',
]}
publishTime={new Date('2023-05-11').toISOString()}
img={eurovisImgSrc}
getAuthorPage={(author) => {
return `mailto:${author.replace(' ', '')}@example.com`;
}}
>
<!-- Custom hed snippet -->
{#snippet hed()}
<h1>
<div class="body-note">A visual guide to</div>
<div class="title text-6xl font-light tracking-widest">EUROVISION</div>
</h1>
{/snippet}
<!-- Custom dek snippet -->
{#snippet dek()}
<div class="dek">
<p>
Performers from 37 countries are coming together May 9-13 in Liverpool,
England, for the 67th annual Eurovision Song Contest. The winner gets
the trophy and their country gets the right to host next years event,
produced by the European Broadcasting Union (EBU).
</p>
</div>
{/snippet}
</HeroHeadline>
```
Add styles in `global.scss`:
```scss
// global.scss
.custom-hed {
h1 {
.body-note {
color: #ffffff;
}
.title {
color: #ffffff;
text-shadow: 1px 1px 8px #ff7c88;
filter: drop-shadow(0px 0px 12px #ff7c88);
}
}
.dek {
margin-block-start: 1rem;
p {
color: #ffffff;
text-shadow: 1px 1px 8px #ff7c88;
filter: drop-shadow(0px 0px 12px #ff7c88);
}
}
}
```
<Canvas of={HeroHeadlineStories.CustomHed} />

View file

@ -0,0 +1,287 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import HeroHeadline from './HeroHeadline.svelte';
const { Story } = defineMeta({
title: 'Components/Text elements/HeroHeadline',
component: HeroHeadline,
argTypes: {
hedSize: {
control: 'select',
options: ['small', 'normal', 'big', 'bigger', 'biggest'],
},
hedWidth: {
control: 'select',
options: ['normal', 'wide', 'wider', 'widest'],
},
hedAlign: {
control: 'select',
options: ['left', 'center', 'right'],
},
width: {
control: 'select',
options: ['normal', 'wide', 'wider', 'widest'],
},
},
});
</script>
<script lang="ts">
import polarImgSrc from './demo/polar.jpg';
import eurovisImgSrc from './demo/eurovis.jpeg';
import Block from '../Block/Block.svelte';
import SiteHeader from '../SiteHeader/SiteHeader.svelte';
import GraphicBlock from '../GraphicBlock/GraphicBlock.svelte';
import Video from '../Video/Video.svelte';
import CrashMap from './demo/graphics/crash.svelte';
import QuakeMap from './demo/graphics/quakemap.svelte';
</script>
<Story name="Photo hero" exportName="PhotoHero">
<Block width="fluid" class="chromatic-ignore">
<SiteHeader />
</Block>
<HeroHeadline
section="World News"
hed="Reuters Graphics Interactive"
dek="The beginning of a beautiful page"
authors={['Jane Doe', 'John Doe']}
publishTime={new Date('2022-03-04').toISOString()}
img={polarImgSrc}
notes="Photo by REUTERS"
ariaDescription="A photo of a polar bear"
/>
</Story>
<Story name="Transparent header" exportName="TransparentHeader">
<div class="transparent-header">
<Block width="fluid" class="chromatic-ignore">
<SiteHeader />
</Block>
<HeroHeadline
section="World News"
hed="Reuters Graphics Interactive"
dek="The beginning of a beautiful page"
authors={['Jane Doe', 'John Doe']}
publishTime={new Date('2022-03-04').toISOString()}
img={polarImgSrc}
ariaDescription="A photo of a polar bear"
/>
</div>
</Story>
<Story name="Ai2svelte hero" exportName="Ai2svelteHero">
<Block width="fluid" class="chromatic-ignore">
<SiteHeader />
</Block>
<HeroHeadline
hed={'Earthquake devastates Afghanistan'}
hedSize={'big'}
hedWidth="wide"
class="custom-hero mb-0"
authors={[
'Anand Katakam',
'Vijdan Mohammad Kawoosa',
'Adolfo Arranz',
'Wen Foo',
'Simon Scarr',
'Aman Bhargava',
'Jitesh Chowdhury',
'Manas Sharma',
'Aditi Bhandari',
]}
publishTime={new Date('2022-06-24').toISOString()}
>
<GraphicBlock
width="widest"
role="figure"
class="my-0"
ariaDescription="Earthquake impact map"
>
<QuakeMap />
</GraphicBlock>
</HeroHeadline>
<style lang="scss">
.hero-wrapper {
// Make hero shorter than 100vh
--heroHeight: 85svh;
@media (max-width: 960px) {
--heroHeight: 65svh;
}
// For small height
@media (max-height: 850px) {
--heroHeight: 100svh;
}
// Custom hero sizing for landscape mobile
@media (max-width: 960px) and (orientation: landscape) {
--heroHeight: 200svh;
}
.custom-hero.headline {
// Adjust vertical positioning
align-items: flex-end !important;
@media (max-width: 1100px) {
// Adjust line length of title
max-width: var(--normal-column-width) !important;
}
}
}
// Override default fixed height for hero layout in embeds
.hero-wrapper.embedded {
--heroHeight: 1000px;
}
</style>
</Story>
<Story name="Video hero" exportName="VideoHero">
<Block width="fluid" class="chromatic-ignore">
<SiteHeader />
</Block>
<HeroHeadline
class="video-hero"
hed="The conflict in Ethiopia"
hedSize="bigger"
hedWidth="wide"
authors={['Aditi Bhandari ', 'David Lewis']}
publishTime={new Date('2020-12-18').toISOString()}
>
<Video
width="widest"
class="my-0"
showControls={false}
preloadVideo="auto"
playVideoWhenInView={false}
src="https://vm.reuters.tv/9c72e/titlef2ac(425954_R21MP41500).mp4"
poster="https://www.reuters.com/resizer/vexYmtEuXKmfnsCbfS6jSMVbHms=/1080x0/filters:quality(80)/cloudfront-us-east-2.images.arcpublishing.com/reuters/VKJHKJEENVO4DASDND3VLHPV5Y.jpg"
notes="Drone footage from the Village 8 refugee camp in Sudan."
ariaDescription="Aerial footage of people houses in refugee camp"
/>
</HeroHeadline>
<style lang="scss">
.hero-wrapper {
--heroHeight: calc(100svh - 60px);
.video-hero.headline {
header {
// Adjust vertical position as offset from default center
top: calc(50svh - 250px);
}
h1 {
color: #ffd430;
text-shadow: 3px 4px 7px rgba(81, 67, 21, 0.8);
}
}
}
</style>
</Story>
<Story name="Inline hero" exportName="InlineHero">
<Block width="fluid" class="chromatic-ignore">
<SiteHeader />
</Block>
<!-- Set `stacked` to `false` -->
<HeroHeadline
stacked={false}
section="Global news"
hed="The plunge from 29,000 feet"
dek="How China Eastern Airlines flight MU5735 went from an uneventful flight at cruising altitude to disaster in just minutes."
class="mb-0"
authors={['Simon Scarr', 'Vijdan Mohammad Kawoosa']}
publishTime={new Date('2020-01-01').toISOString()}
>
<GraphicBlock
width="widest"
role="figure"
class="my-0"
ariaDescription="Earthquake impact map"
notes="Source: Satellite image from Google, Maxar Technologies, CNES/Airbus, Landsat/Copernicus"
>
<CrashMap />
</GraphicBlock>
</HeroHeadline>
</Story>
<Story name="Custom hed" exportName="CustomHed">
<HeroHeadline
class="custom-hed"
authors={[
'Prasanta Kumar Dutta',
'Dea Bankova',
'Aditi Bhandari',
'Anurag Rao',
]}
publishTime={new Date('2023-05-11').toISOString()}
img={eurovisImgSrc}
getAuthorPage={(author: string) => {
return `mailto:${author.replace(' ', '')}@example.com`;
}}
>
{#snippet hed()}
<h1>
<div class="body-note">A visual guide to</div>
<div class="title text-6xl font-light tracking-widest">EUROVISION</div>
</h1>
{/snippet}
{#snippet dek()}
<div class="dek">
<p>
Performers from 37 countries are coming together May 9-13 in
Liverpool, England, for the 67th annual Eurovision Song Contest. The
winner gets the trophy and their country gets the right to host next
years event, produced by the European Broadcasting Union (EBU).
</p>
</div>
{/snippet}
</HeroHeadline>
<style lang="scss">
.custom-hed {
h1 {
.body-note {
color: #ffffff;
}
.title {
color: #ffffff;
text-shadow: 1px 1px 8px #ff7c88;
filter: drop-shadow(0px 0px 12px #ff7c88);
}
}
.dek {
margin-block-start: 1rem;
p {
color: #ffffff;
text-shadow: 1px 1px 8px #ff7c88;
filter: drop-shadow(0px 0px 12px #ff7c88);
}
}
}
</style>
</Story>
<style lang="scss">
.transparent-header {
:global(.nav-container) {
background-color: transparent !important;
}
:global(.nav-container .inner) {
background-color: transparent !important;
border: none !important;
}
:global(.hero-wrapper) {
margin-block-start: -64px;
}
}
</style>

View file

@ -0,0 +1,276 @@
<!-- @component `HeroHeadline` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-text-elements-heroheadline--docs) -->
<script lang="ts">
import type { Snippet } from 'svelte';
import type { HeadlineSize } from '../@types/global';
// Components
import Block from '../Block/Block.svelte';
import GraphicBlock from '../GraphicBlock/GraphicBlock.svelte';
import PaddingReset from '../PaddingReset/PaddingReset.svelte';
import Headline from '../Headline/Headline.svelte';
import Byline from '../Byline/Byline.svelte';
import FeaturePhoto from '../FeaturePhoto/FeaturePhoto.svelte';
export interface Props {
/** Headline, parsed as an _inline_ markdown string in an `h1` element OR as a custom snippet. */
hed: string | Snippet;
/**
* Optional snippet for a custom hero graphic, photo, etc.
*/
children?: Snippet;
/** Set to `false` for inline hero media */
stacked?: boolean;
/**
* Path to the background hero image
*/
img?: string;
/**
* ARIA description, passed in as a markdown string.
*/
ariaDescription?: string;
/**
* Notes to the graphic, passed in as a markdown string.
*/
notes?: string;
/** Add classes to the block tag to target it with custom CSS. */
class?: string;
/**
* Headline size: small, normal, big, bigger, biggest
*/
hedSize?: HeadlineSize;
/**
* Headline horizontal alignment: left, center, right
*/
hedAlign?: 'left' | 'center' | 'right';
/**
* Width of the headline: normal, wide, wider, widest
*/
hedWidth?: 'normal' | 'wide' | 'wider' | 'widest';
/**
* Dek, parsed as a markdown string OR as a custom snippet.
*/
dek?: string | Snippet;
/**
* Section title
*/
section?: 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;
/**
* Width of the hero graphic: normal, wide, wider, widest
*/
width?: 'normal' | 'wide' | 'wider' | 'widest';
/** Set to true for embeds. */
embedded?: boolean;
/**
* 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 {
hed,
stacked = true,
img,
ariaDescription,
notes,
class: cls = '',
hedSize = 'normal',
hedAlign = 'center',
hedWidth = 'normal',
dek,
section,
authors = [],
publishTime,
updateTime,
width = 'widest',
embedded = false,
children,
getAuthorPage,
byline,
published,
updated,
}: Props = $props();
</script>
<div style="--heroHeight: {embedded ? '850px' : '100svh'}; display:contents;">
<div class="hero-wrapper fmb-6" class:embedded>
<!-- stacked media hero-->
{#if stacked}
<Block width="fluid" class="hero-headline background-hero fmt-0">
<!-- Handles string or snippet `hed` -->
<Headline
class="{cls} !text-{hedAlign}"
width={hedWidth}
{section}
{hedSize}
{hed}
{dek}
/>
<div class="graphic-container">
<!-- Custom hero snippet -->
{#if children}
{@render children()}
<!-- Otherwise render the image if it exists -->
{:else if img}
<GraphicBlock
{width}
role="img"
class="my-0"
textWidth="normal"
{notes}
{ariaDescription}
>
<div
class="background-image"
style="background-image: url({img})"
></div>
</GraphicBlock>
{/if}
</div>
</Block>
{/if}
<!-- Non-stacked hero -->
{#if stacked === false}
<Block width="fluid" class="hero-headline inline-hero">
<PaddingReset containerIsFluid={true}>
<Headline
class="{cls} !text-{hedAlign}"
width={hedWidth}
{section}
{hedSize}
{hed}
{dek}
/>
</PaddingReset>
<div class="graphic-container">
<!-- Custom hero snippet -->
{#if children}
{@render children()}
<!-- Otherwise render the image if it exists -->
{:else if img}
<FeaturePhoto
{width}
class="my-0"
src={img}
caption={notes}
altText={ariaDescription || ''}
/>
{/if}
</div>
</Block>
{/if}
</div>
<div class="hero-byline fmb-6">
{#if byline}
<!-- Custom byline/dateline -->
{@render byline()}
{:else if authors.length > 0 || publishTime}
<Byline
{authors}
{publishTime}
{updateTime}
{getAuthorPage}
{published}
{updated}
align="auto"
/>
{/if}
</div>
</div>
<style lang="scss">
@use 'mixins' as mixins;
.hero-wrapper {
:global(.background-hero) {
min-height: var(--heroHeight, 100svh);
max-height: 1800px;
position: relative;
}
:global(.background-hero .headline) {
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
width: 100%;
top: 0;
left: 50%;
height: var(--heroHeight, 100svh);
max-height: 1800px;
transform: translateX(-50%);
@include mixins.fmt-0;
@media (max-width: 690px) {
padding: 0 15px;
}
}
:global(aside p) {
@include mixins.body-caption;
}
:global(.background-hero video) {
position: relative;
display: block;
width: 100%;
height: var(--heroHeight);
object-fit: cover;
}
:global(.graphic-container .article-block.notes) {
@media (max-width: 690px) {
width: 100%;
padding: 0 15px;
margin-inline-start: 0;
}
}
}
.hero-byline {
:global(.byline-container) {
z-index: 1;
position: relative;
}
}
.background-image {
width: auto;
height: var(--heroHeight, 100svh);
max-height: 1800px;
user-select: none;
background-repeat: no-repeat;
background-position: center;
background-size: cover;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

View file

@ -0,0 +1,650 @@
<script>
let width = $state();
// @ts-ignore img
import chartXs from './CRASH_1-xs.jpeg';
// @ts-ignore img
import chartSm from './CRASH_1-sm.jpeg';
// @ts-ignore img
import chartMd from './CRASH_1-md.jpeg';
// @ts-ignore img
import chartLg from './CRASH_1-lg.jpeg';
// @ts-ignore img
import chartXl from './CRASH_1-xl.jpeg';
// @ts-ignore img
import chartXxl from './CRASH_1-xl_copy.jpeg';
</script>
<!-- Generated by ai2html v0.100.0 - 2022-03-29 17:01 -->
<div id="g-CRASH_1-box" bind:clientWidth={width}>
<!-- Artboard: xs -->
{#if width && width >= 0 && width < 510}
<div id="g-CRASH_1-xs" class="g-artboard" style="">
<div style="padding: 0 0 93.0303% 0;"></div>
<div
id="g-CRASH_1-xs-img"
class="g-aiImg"
style={`background-image: url(${chartXs});`}
></div>
<div
id="g-ai0-1"
class="g-xs-text g-aiAbs g-aiPointText"
style="top:18.0723%;margin-top:-14.5px;left:7.0539%;width:76px;"
>
<p class="g-pstyle0">Cruising at</p>
<p class="g-pstyle0">29,100 feet</p>
</div>
<div
id="g-ai0-2"
class="g-xs-text g-aiAbs g-aiPointText"
style="top:20.271%;margin-top:-21.2px;left:26.8941%;width:103px;"
>
<p class="g-pstyle1">2.21 pm</p>
<p class="g-pstyle0">Steep drop from</p>
<p class="g-pstyle0">27,025 feet</p>
</div>
<div
id="g-ai0-3"
class="g-xs-text g-aiAbs g-aiPointText"
style="top:42.7133%;margin-top:-15.1px;right:3.3749%;width:96px;"
>
<p class="g-pstyle2">Typical path to</p>
<p class="g-pstyle2">Guangzhou</p>
</div>
<div
id="g-ai0-4"
class="g-xs-text g-aiAbs g-aiPointText"
style="top:70.482%;margin-top:-15.4px;right:2.4487%;width:79px;"
>
<p class="g-pstyle2">Last known</p>
<p class="g-pstyle2">location</p>
</div>
<div
id="g-ai0-5"
class="g-xs-text g-aiAbs g-aiPointText"
style="top:87.4202%;margin-top:-15.4px;right:16.3453%;width:50px;"
>
<p class="g-pstyle2">Crash</p>
<p class="g-pstyle2">site</p>
</div>
</div>
{/if}
<!-- Artboard: sm -->
{#if width && width >= 510 && width < 660}
<div id="g-CRASH_1-sm" class="g-artboard" style="">
<div style="padding: 0 0 77.8431% 0;"></div>
<div
id="g-CRASH_1-sm-img"
class="g-aiImg"
style={`background-image: url(${chartSm});`}
></div>
<div
id="g-ai1-1"
class="g-sm-text g-aiAbs g-aiPointText"
style="top:17.3436%;margin-top:-16.9px;left:16.4992%;width:86px;"
>
<p class="g-pstyle0">Cruising at</p>
<p class="g-pstyle0">29,100 feet</p>
</div>
<div
id="g-ai1-2"
class="g-sm-text g-aiAbs g-aiPointText"
style="top:20.87%;margin-top:-24.9px;left:32.8523%;width:117px;"
>
<p class="g-pstyle1">2.21 pm</p>
<p class="g-pstyle0">Steep drop from</p>
<p class="g-pstyle0">27,025 feet</p>
</div>
<div
id="g-ai1-3"
class="g-sm-text g-aiAbs g-aiPointText"
style="top:43.9824%;margin-top:-17.6px;right:4.7685%;width:108px;"
>
<p class="g-pstyle2">Typical path to</p>
<p class="g-pstyle2">Guangzhou</p>
</div>
<div
id="g-ai1-4"
class="g-sm-text g-aiAbs g-aiPointText"
style="top:71.9421%;margin-top:-17.6px;right:4.6892%;width:89px;"
>
<p class="g-pstyle2">Last known</p>
<p class="g-pstyle2">location</p>
</div>
<div
id="g-ai1-5"
class="g-sm-text g-aiAbs g-aiPointText"
style="top:87.0554%;margin-top:-17.6px;right:19.7924%;width:55px;"
>
<p class="g-pstyle2">Crash</p>
<p class="g-pstyle2">site</p>
</div>
</div>
{/if}
<!-- Artboard: md -->
{#if width && width >= 660 && width < 930}
<div id="g-CRASH_1-md" class="g-artboard" style="">
<div style="padding: 0 0 68.7879% 0;"></div>
<div
id="g-CRASH_1-md-img"
class="g-aiImg"
style={`background-image: url(${chartMd});`}
></div>
<div
id="g-ai2-1"
class="g-md-text g-aiAbs g-aiPointText"
style="top:17.1581%;margin-top:-13.9px;left:3.6766%;width:76px;"
>
<p class="g-pstyle0">Cruising at</p>
<p class="g-pstyle0">29,100 feet</p>
</div>
<div
id="g-ai2-2"
class="g-md-text g-aiAbs g-aiPointText"
style="top:20.0216%;margin-top:-13.9px;left:19.6076%;width:92px;"
>
<p class="g-pstyle1">2.20 pm</p>
<p class="g-pstyle0">Slight descent</p>
</div>
<div
id="g-ai2-3"
class="g-md-text g-aiAbs g-aiPointText"
style="top:24.1477%;margin-top:-20.6px;left:34.5493%;width:102px;"
>
<p class="g-pstyle1">2.21 pm</p>
<p class="g-pstyle0">Steep drop from</p>
<p class="g-pstyle0">27,025 feet</p>
</div>
<div
id="g-ai2-4"
class="g-md-text g-aiAbs g-aiPointText"
style="top:46.8149%;margin-top:-14.5px;right:2.8165%;width:95px;"
>
<p class="g-pstyle2">Typical path to</p>
<p class="g-pstyle2">Guangzhou</p>
</div>
<div
id="g-ai2-5"
class="g-md-text g-aiAbs g-aiPointText"
style="top:80.0748%;margin-top:-14.5px;left:84.28%;width:79px;"
>
<p class="g-pstyle0">Last known</p>
<p class="g-pstyle0">location</p>
</div>
<div
id="g-ai2-6"
class="g-md-text g-aiAbs g-aiPointText"
style="top:87.5638%;margin-top:-14.5px;right:22.3457%;width:86px;"
>
<p class="g-pstyle2">Approximate</p>
<p class="g-pstyle2">crash site</p>
</div>
</div>
{/if}
<!-- Artboard: lg -->
{#if width && width >= 930 && width < 1200}
<div id="g-CRASH_1-lg" class="g-artboard" style="">
<div style="padding: 0 0 61.3978% 0;"></div>
<div
id="g-CRASH_1-lg-img"
class="g-aiImg"
style={`background-image: url(${chartLg});`}
></div>
<div
id="g-ai3-1"
class="g-lg-text g-aiAbs g-aiPointText"
style="top:16.9729%;margin-top:-17.9px;left:4.0448%;width:90px;"
>
<p class="g-pstyle0">Cruising at</p>
<p class="g-pstyle0">29,100 feet</p>
</div>
<div
id="g-ai3-2"
class="g-lg-text g-aiAbs g-aiPointText"
style="top:20.3004%;margin-top:-17.9px;left:27.525%;width:111px;"
>
<p class="g-pstyle1">2.20 pm</p>
<p class="g-pstyle2">Slight descent</p>
</div>
<div
id="g-ai3-3"
class="g-lg-text g-aiAbs g-aiPointText"
style="top:24.5911%;margin-top:-26.4px;left:40.9124%;width:124px;"
>
<p class="g-pstyle1">2.21 pm</p>
<p class="g-pstyle2">Steep drop from</p>
<p class="g-pstyle2">27,025 feet</p>
</div>
<div
id="g-ai3-4"
class="g-lg-text g-aiAbs g-aiPointText"
style="top:47.2373%;margin-top:-18.7px;right:2.986%;width:114px;"
>
<p class="g-pstyle3">Typical path to</p>
<p class="g-pstyle3">Guangzhou</p>
</div>
<div
id="g-ai3-5"
class="g-lg-text g-aiAbs g-aiPointText"
style="top:80.6874%;margin-top:-18.7px;left:85.4704%;width:94px;"
>
<p class="g-pstyle2">Last known</p>
<p class="g-pstyle2">location</p>
</div>
<div
id="g-ai3-6"
class="g-lg-text g-aiAbs g-aiPointText"
style="top:88.0429%;margin-top:-18.7px;right:20.5522%;width:102px;"
>
<p class="g-pstyle3">Approximate</p>
<p class="g-pstyle3">crash site</p>
</div>
</div>
{/if}
<!-- Artboard: xl -->
{#if width && width >= 1200 && width < 1350}
<div id="g-CRASH_1-xl" class="g-artboard" style="">
<div style="padding: 0 0 47% 0;"></div>
<div
id="g-CRASH_1-xl-img"
class="g-aiImg"
style={`background-image: url(${chartXl});`}
></div>
<div
id="g-ai4-1"
class="g-xl-text g-aiAbs g-aiPointText"
style="top:16.4851%;margin-top:-19px;left:21.0319%;width:95px;"
>
<p class="g-pstyle0">Cruising at</p>
<p class="g-pstyle0">29,100 feet</p>
</div>
<div
id="g-ai4-2"
class="g-xl-text g-aiAbs g-aiPointText"
style="top:20.1977%;margin-top:-17.9px;left:38.5203%;width:111px;"
>
<p class="g-pstyle1">2.20 pm</p>
<p class="g-pstyle2">Slight descent</p>
</div>
<div
id="g-ai4-3"
class="g-xl-text g-aiAbs g-aiPointText"
style="top:24.5417%;margin-top:-26.4px;left:48.8956%;width:124px;"
>
<p class="g-pstyle1">2.21 pm</p>
<p class="g-pstyle2">Steep drop from</p>
<p class="g-pstyle2">27,025 feet</p>
</div>
<div
id="g-ai4-4"
class="g-xl-text g-aiAbs g-aiPointText"
style="top:53.1427%;margin-top:-18.7px;right:2.0477%;width:114px;"
>
<p class="g-pstyle3">Typical path to</p>
<p class="g-pstyle3">Guangzhou</p>
</div>
<div
id="g-ai4-5"
class="g-xl-text g-aiAbs g-aiPointText"
style="top:81.3342%;margin-top:-18.7px;left:83.4281%;width:94px;"
>
<p class="g-pstyle2">Last known</p>
<p class="g-pstyle2">location</p>
</div>
<div
id="g-ai4-6"
class="g-xl-text g-aiAbs g-aiPointText"
style="top:88.781%;margin-top:-18.7px;right:21.2395%;width:102px;"
>
<p class="g-pstyle3">Approximate</p>
<p class="g-pstyle3">crash site</p>
</div>
</div>
{/if}
<!-- Artboard: xl_copy -->
{#if width && width >= 1350}
<div id="g-CRASH_1-xl_copy" class="g-artboard" style="">
<div style="padding: 0 0 46.8889% 0;"></div>
<div
id="g-CRASH_1-xl_copy-img"
class="g-aiImg"
style={`background-image: url(${chartXxl});`}
></div>
<div
id="g-ai5-1"
class="g-xxl-text g-aiAbs g-aiPointText"
style="top:13.5823%;margin-top:-19px;left:17.5449%;width:95px;"
>
<p class="g-pstyle0">Cruising at</p>
<p class="g-pstyle0">29,100 feet</p>
</div>
<div
id="g-ai5-2"
class="g-xxl-text g-aiAbs g-aiPointText"
style="top:16.11%;margin-top:-19px;left:34.2801%;width:117px;"
>
<p class="g-pstyle1">2.20 pm</p>
<p class="g-pstyle0">Slight descent</p>
</div>
<div
id="g-ai5-3"
class="g-xxl-text g-aiAbs g-aiPointText"
style="top:20.5333%;margin-top:-28px;left:45.3329%;width:130px;"
>
<p class="g-pstyle1">2.21 pm</p>
<p class="g-pstyle0">Steep drop from</p>
<p class="g-pstyle0">27,025 feet</p>
</div>
<div
id="g-ai5-4"
class="g-xxl-text g-aiAbs g-aiPointText"
style="top:51.1596%;margin-top:-19.8px;right:2.3384%;width:121px;"
>
<p class="g-pstyle2">Typical path to</p>
<p class="g-pstyle2">Guangzhou</p>
</div>
<div
id="g-ai5-5"
class="g-xxl-text g-aiAbs g-aiPointText"
style="top:81.3333%;margin-top:-19.8px;left:82.1208%;width:98px;"
>
<p class="g-pstyle0">Last known</p>
<p class="g-pstyle0">location</p>
</div>
<div
id="g-ai5-6"
class="g-xxl-text g-aiAbs g-aiPointText"
style="top:89.3902%;margin-top:-19.8px;right:22.7998%;width:108px;"
>
<p class="g-pstyle2">Approximate</p>
<p class="g-pstyle2">crash site</p>
</div>
</div>
{/if}
</div>
<!-- End ai2html - 2022-03-29 17:01 -->
<!-- ai file: CRASH_1.ai -->
<style lang="scss">
#g-CRASH_1-box,
#g-CRASH_1-box .g-artboard {
margin: 0 auto;
}
#g-CRASH_1-box p {
margin: 0;
}
#g-CRASH_1-box .g-aiAbs {
position: absolute;
}
#g-CRASH_1-box .g-aiImg {
position: absolute;
top: 0;
display: block;
width: 100% !important;
height: 100%;
background-size: contain;
background-repeat: no-repeat;
}
#g-CRASH_1-box .g-aiPointText p {
white-space: nowrap;
}
#g-CRASH_1-xs {
position: relative;
overflow: hidden;
}
#g-CRASH_1-xs p {
font-family:
'Source Sans Pro',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
'Helvetica Neue',
Helvetica,
Arial,
sans-serif;
font-weight: 400;
line-height: 14px;
height: auto;
opacity: 1;
letter-spacing: 0em;
font-size: 12px;
text-align: left;
color: rgb(255, 255, 255);
text-transform: none;
padding-bottom: 0;
padding-top: 0;
mix-blend-mode: normal;
font-style: normal;
position: static;
}
#g-CRASH_1-xs .g-pstyle0 {
height: 14px;
}
#g-CRASH_1-xs .g-pstyle1 {
font-weight: 700;
height: 14px;
}
#g-CRASH_1-xs .g-pstyle2 {
height: 14px;
text-align: right;
}
#g-CRASH_1-sm {
position: relative;
overflow: hidden;
}
#g-CRASH_1-sm p {
font-family:
'Source Sans Pro',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
'Helvetica Neue',
Helvetica,
Arial,
sans-serif;
font-weight: 400;
line-height: 16px;
height: auto;
opacity: 1;
letter-spacing: 0em;
font-size: 14px;
text-align: left;
color: rgb(255, 255, 255);
text-transform: none;
padding-bottom: 0;
padding-top: 0;
mix-blend-mode: normal;
font-style: normal;
position: static;
}
#g-CRASH_1-sm .g-pstyle0 {
height: 16px;
}
#g-CRASH_1-sm .g-pstyle1 {
font-weight: 700;
height: 16px;
}
#g-CRASH_1-sm .g-pstyle2 {
height: 16px;
text-align: right;
}
#g-CRASH_1-md {
position: relative;
overflow: hidden;
}
#g-CRASH_1-md p {
font-family:
'Source Sans Pro',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
'Helvetica Neue',
Helvetica,
Arial,
sans-serif;
font-weight: 400;
line-height: 13px;
height: auto;
opacity: 1;
letter-spacing: 0em;
font-size: 12px;
text-align: left;
color: rgb(255, 255, 255);
text-transform: none;
padding-bottom: 0;
padding-top: 0;
mix-blend-mode: normal;
font-style: normal;
position: static;
}
#g-CRASH_1-md .g-pstyle0 {
height: 13px;
}
#g-CRASH_1-md .g-pstyle1 {
font-weight: 700;
height: 13px;
}
#g-CRASH_1-md .g-pstyle2 {
height: 13px;
text-align: right;
}
#g-CRASH_1-lg {
position: relative;
overflow: hidden;
}
#g-CRASH_1-lg p {
font-family:
'Source Sans Pro',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
'Helvetica Neue',
Helvetica,
Arial,
sans-serif;
font-weight: 400;
line-height: 17px;
height: auto;
opacity: 1;
letter-spacing: 0em;
font-size: 15px;
text-align: left;
color: rgb(255, 255, 255);
text-transform: none;
padding-bottom: 0;
padding-top: 0;
mix-blend-mode: normal;
font-style: normal;
position: static;
}
#g-CRASH_1-lg .g-pstyle0 {
height: 17px;
color: rgb(88, 89, 91);
}
#g-CRASH_1-lg .g-pstyle1 {
font-weight: 700;
height: 17px;
}
#g-CRASH_1-lg .g-pstyle2 {
height: 17px;
}
#g-CRASH_1-lg .g-pstyle3 {
height: 17px;
text-align: right;
}
#g-CRASH_1-xl {
position: relative;
overflow: hidden;
}
#g-CRASH_1-xl p {
font-family:
'Source Sans Pro',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
'Helvetica Neue',
Helvetica,
Arial,
sans-serif;
font-weight: 400;
line-height: 17px;
height: auto;
opacity: 1;
letter-spacing: 0em;
font-size: 15px;
text-align: left;
color: rgb(255, 255, 255);
text-transform: none;
padding-bottom: 0;
padding-top: 0;
mix-blend-mode: normal;
font-style: normal;
position: static;
}
#g-CRASH_1-xl .g-pstyle0 {
line-height: 18px;
height: 18px;
font-size: 16px;
}
#g-CRASH_1-xl .g-pstyle1 {
font-weight: 700;
height: 17px;
}
#g-CRASH_1-xl .g-pstyle2 {
height: 17px;
}
#g-CRASH_1-xl .g-pstyle3 {
height: 17px;
text-align: right;
}
#g-CRASH_1-xl_copy {
position: relative;
overflow: hidden;
}
#g-CRASH_1-xl_copy p {
font-family:
'Source Sans Pro',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
'Helvetica Neue',
Helvetica,
Arial,
sans-serif;
font-weight: 400;
line-height: 18px;
height: auto;
opacity: 1;
letter-spacing: 0em;
font-size: 16px;
text-align: left;
color: rgb(255, 255, 255);
text-transform: none;
padding-bottom: 0;
padding-top: 0;
mix-blend-mode: normal;
font-style: normal;
position: static;
}
#g-CRASH_1-xl_copy .g-pstyle0 {
height: 18px;
}
#g-CRASH_1-xl_copy .g-pstyle1 {
font-weight: 700;
height: 18px;
}
#g-CRASH_1-xl_copy .g-pstyle2 {
height: 18px;
text-align: right;
}
/* Custom CSS */
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

View file

@ -0,0 +1,863 @@
<script>
let width = $state();
// @ts-ignore raw
import chartXs from './quake-map-top-xs.jpeg';
// @ts-ignore raw
import chartSm from './quake-map-top-sm.jpeg';
// @ts-ignore raw
import chartMd from '././quake-map-top-md.jpeg';
// @ts-ignore raw
import chartLg from '././quake-map-top-lg.jpeg';
// @ts-ignore raw
import chartXl from '././quake-map-top-xl.jpeg';
</script>
<div id="g-quake-map-top-box" bind:clientWidth={width}>
<!-- Artboard: xs -->
{#if width && width >= 0 && width < 510}
<div id="g-quake-map-top-xs" class="g-artboard" style="">
<div style="padding: 0 0 117.5758% 0;"></div>
<div
id="g-quake-map-top-xs-img"
class="g-aiImg"
style={`background-image: url(${chartXs});`}
></div>
<div
id="g-ai0-1"
class="g-tt g-aiAbs g-aiPointText"
style="top:-1.3529%;margin-top:-6.8px;left:33.0848%;width:46px;"
>
<p class="g-pstyle0">Kabul</p>
</div>
<div
id="g-ai0-2"
class="g-tt g-aiAbs g-aiPointText"
style="top:4.3062%;margin-top:-7.7px;left:4.0902%;width:65px;"
>
<p class="g-pstyle1">Shaking</p>
</div>
<div
id="g-ai0-3"
class="g-tt g-aiAbs g-aiPointText"
style="top:8.1722%;margin-top:-7.7px;left:4.0902%;width:78px;"
>
<p class="g-pstyle2">Very strong</p>
</div>
<div
id="g-ai0-4"
class="g-tt g-aiAbs g-aiPointText"
style="top:19.5124%;margin-top:-7.7px;left:4.0902%;width:49px;"
>
<p class="g-pstyle2">Weak</p>
</div>
<div
id="g-ai0-5"
class="g-tt g-aiAbs g-aiPointText"
style="top:22.4511%;margin-top:-5.1px;left:45.2353%;margin-left:-74px;width:148px;"
>
<p class="g-pstyle3">AFGHANISTAN</p>
</div>
<div
id="g-ai0-6"
class="g-tt g-aiAbs g-aiPointText"
style="top:29.7371%;margin-top:-8.4px;left:34.8676%;width:56px;"
>
<p class="g-pstyle4">Gardez</p>
</div>
<div
id="g-ai0-7"
class="g-tt g-aiAbs g-aiPointText"
style="top:39.2732%;margin-top:-8.4px;left:65.8508%;width:50px;"
>
<p class="g-pstyle4">Khost</p>
</div>
<div
id="g-ai0-8"
class="g-tt g-aiAbs g-aiPointText"
style="top:45.9742%;margin-top:-8.4px;left:46.1799%;width:73px;"
>
<p class="g-pstyle5">Epicentre</p>
</div>
<div
id="g-ai0-9"
class="g-tt g-aiAbs g-aiPointText"
style="top:49.8402%;margin-top:-8.4px;left:93.2747%;margin-left:-27px;width:54px;"
>
<p class="g-pstyle6">Bannu</p>
</div>
<div
id="g-ai0-10"
class="g-tt g-aiAbs g-aiPointText"
style="top:63.4369%;margin-top:-5.1px;left:65.9996%;margin-left:-50px;width:100px;"
>
<p class="g-pstyle7">PAKISTAN</p>
</div>
</div>
{/if}
<!-- Artboard: sm -->
{#if width && width >= 510 && width < 660}
<div id="g-quake-map-top-sm" class="g-artboard" style="">
<div style="padding: 0 0 83.5294% 0;"></div>
<div
id="g-quake-map-top-sm-img"
class="g-aiImg"
style={`background-image: url(${chartSm});`}
></div>
<div
id="g-ai1-1"
class="g-tt g-aiAbs g-aiPointText"
style="top:5.3132%;margin-top:-8.6px;left:3.5997%;width:71px;"
>
<p class="g-pstyle0">Shaking</p>
</div>
<div
id="g-ai1-2"
class="g-tt g-aiAbs g-aiPointText"
style="top:9.3038%;margin-top:-8.6px;left:3.5997%;width:86px;"
>
<p class="g-pstyle1">Very strong</p>
</div>
<div
id="g-ai1-3"
class="g-tt g-aiAbs g-aiPointText"
style="top:18.8592%;margin-top:-5.3px;left:41.2699%;margin-left:-91.5px;width:183px;"
>
<p class="g-pstyle2">AFGHANISTAN</p>
</div>
<div
id="g-ai1-4"
class="g-tt g-aiAbs g-aiPointText"
style="top:21.2757%;margin-top:-8.6px;left:3.5997%;width:53px;"
>
<p class="g-pstyle1">Weak</p>
</div>
<div
id="g-ai1-5"
class="g-tt g-aiAbs g-aiPointText"
style="top:30.6101%;margin-top:-9.4px;left:40.186%;width:61px;"
>
<p class="g-pstyle3">Gardez</p>
</div>
<div
id="g-ai1-6"
class="g-tt g-aiAbs g-aiPointText"
style="top:34.8355%;margin-top:-9.4px;left:9.9042%;width:60px;"
>
<p class="g-pstyle3">Ghazni</p>
</div>
<div
id="g-ai1-7"
class="g-tt g-aiAbs g-aiPointText"
style="top:40.4693%;margin-top:-9.4px;left:63.0024%;width:54px;"
>
<p class="g-pstyle3">Khost</p>
</div>
<div
id="g-ai1-8"
class="g-tt g-aiAbs g-aiPointText"
style="top:47.2768%;margin-top:-9.4px;left:48.5165%;width:80px;"
>
<p class="g-pstyle4">Epicentre</p>
</div>
<div
id="g-ai1-9"
class="g-tt g-aiAbs g-aiPointText"
style="top:50.7979%;margin-top:-9.4px;left:82.1761%;margin-left:-29px;width:58px;"
>
<p class="g-pstyle5">Bannu</p>
</div>
<div
id="g-ai1-10"
class="g-tt g-aiAbs g-aiPointText"
style="top:68.3897%;margin-top:-5.3px;left:73.6919%;margin-left:-67.5px;width:135px;"
>
<p class="g-pstyle2">PAKISTAN</p>
</div>
</div>
{/if}
<!-- Artboard: md -->
{#if width && width >= 660 && width < 1200}
<div id="g-quake-map-top-md" class="g-artboard" style="">
<div style="padding: 0 0 91.8182% 0;"></div>
<div
id="g-quake-map-top-md-img"
class="g-aiImg"
style={`background-image: url(${chartMd});`}
></div>
<div
id="g-ai2-1"
class="g-tt g-aiAbs g-aiPointText"
style="top:3.5477%;margin-top:-7.5px;left:2.6635%;width:62px;"
>
<p class="g-pstyle0">Shaking</p>
</div>
<div
id="g-ai2-2"
class="g-tt g-aiAbs g-aiPointText"
style="top:6.188%;margin-top:-7.5px;left:2.6635%;width:74px;"
>
<p class="g-pstyle1">Very strong</p>
</div>
<div
id="g-ai2-3"
class="g-tt g-aiAbs g-aiPointText"
style="top:7.0759%;margin-top:-6.9px;right:6.4574%;width:100px;"
>
<p class="g-pstyle2">Afghanistan</p>
</div>
<div
id="g-ai2-4"
class="g-tt g-aiAbs g-aiPointText"
style="top:15.2639%;margin-top:-7.5px;left:2.6635%;width:47px;"
>
<p class="g-pstyle1">Weak</p>
</div>
<div
id="g-ai2-5"
class="g-tt g-aiAbs g-aiPointText"
style="top:18.7377%;margin-top:-7.6px;left:38.071%;margin-left:-136.5px;width:273px;"
>
<p class="g-pstyle3">AFGHANISTAN</p>
</div>
<div
id="g-ai2-6"
class="g-tt g-aiAbs g-aiPointText"
style="top:30.2476%;margin-top:-10.3px;left:38.0816%;width:63px;"
>
<p class="g-pstyle4">Gardez</p>
</div>
<div
id="g-ai2-7"
class="g-tt g-aiAbs g-aiPointText"
style="top:33.8779%;margin-top:-10.3px;left:9.3056%;margin-left:-31px;width:62px;"
>
<p class="g-pstyle5">Ghazni</p>
</div>
<div
id="g-ai2-8"
class="g-tt g-aiAbs g-aiPointText"
style="top:40.1486%;margin-top:-10.3px;left:63.0563%;width:55px;"
>
<p class="g-pstyle4">Khost</p>
</div>
<div
id="g-ai2-9"
class="g-tt g-aiAbs g-aiPointText"
style="top:47.1569%;margin-top:-10.8px;left:47.4444%;width:86px;"
>
<p class="g-pstyle6">Epicentre</p>
</div>
<div
id="g-ai2-10"
class="g-tt g-aiAbs g-aiPointText"
style="top:50.8746%;margin-top:-10.3px;left:84.5785%;margin-left:-30px;width:60px;"
>
<p class="g-pstyle5">Bannu</p>
</div>
<div
id="g-ai2-11"
class="g-tt g-aiAbs g-aiPointText"
style="top:62.467%;margin-top:-7.6px;left:80.6735%;margin-left:-100px;width:200px;"
>
<p class="g-pstyle3">PAKISTAN</p>
</div>
</div>
{/if}
<!-- Artboard: lg -->
{#if width && width >= 1200 && width < 1300}
<div id="g-quake-map-top-lg" class="g-artboard" style="">
<div style="padding: 0 0 55.1667% 0;"></div>
<div
id="g-quake-map-top-lg-img"
class="g-aiImg"
style={`background-image: url(${chartLg});`}
></div>
<div
id="g-ai3-1"
class="g-tt g-aiAbs g-aiPointText"
style="top:4.1944%;margin-top:-8.8px;left:1.7817%;width:67px;"
>
<p class="g-pstyle0">Shaking</p>
</div>
<div
id="g-ai3-2"
class="g-tt g-aiAbs g-aiPointText"
style="top:6.9135%;margin-top:-8.8px;left:1.7817%;width:81px;"
>
<p class="g-pstyle1">Very strong</p>
</div>
<div
id="g-ai3-3"
class="g-tt g-aiAbs g-aiPointText"
style="top:7.006%;margin-top:-7.4px;right:4.4122%;width:100px;"
>
<p class="g-pstyle2">Afghanistan</p>
</div>
<div
id="g-ai3-4"
class="g-tt g-aiAbs g-aiPointText"
style="top:14.111%;margin-top:-8.4px;left:29.9587%;margin-left:-151px;width:302px;"
>
<p class="g-pstyle3">AFGHANISTAN</p>
</div>
<div
id="g-ai3-5"
class="g-tt g-aiAbs g-aiPointText"
style="top:16.128%;margin-top:-8.8px;left:1.7818%;width:51px;"
>
<p class="g-pstyle1">Weak</p>
</div>
<div
id="g-ai3-6"
class="g-tt g-aiAbs g-aiPointText"
style="top:28.2991%;margin-top:-11.3px;left:44.1554%;width:67px;"
>
<p class="g-pstyle4">Gardez</p>
</div>
<div
id="g-ai3-7"
class="g-tt g-aiAbs g-aiPointText"
style="top:31.9244%;margin-top:-11.3px;left:26.4286%;margin-left:-33px;width:66px;"
>
<p class="g-pstyle5">Ghazni</p>
</div>
<div
id="g-ai3-8"
class="g-tt g-aiAbs g-aiPointText"
style="top:37.676%;margin-top:-8.4px;left:85.4421%;margin-left:-110.5px;width:221px;"
>
<p class="g-pstyle3">PAKISTAN</p>
</div>
<div
id="g-ai3-9"
class="g-tt g-aiAbs g-aiPointText"
style="top:38.42%;margin-top:-11.3px;left:59.5142%;width:59px;"
>
<p class="g-pstyle4">Khost</p>
</div>
<div
id="g-ai3-10"
class="g-tt g-aiAbs g-aiPointText"
style="top:45.5991%;margin-top:-11.9px;left:49.9132%;width:94px;"
>
<p class="g-pstyle6">Epicentre</p>
</div>
<div
id="g-ai3-11"
class="g-tt g-aiAbs g-aiPointText"
style="top:53.9788%;margin-top:-11.3px;left:72.7294%;margin-left:-32px;width:64px;"
>
<p class="g-pstyle5">Bannu</p>
</div>
</div>
{/if}
<!-- Artboard: xl -->
{#if width && width >= 1300}
<div id="g-quake-map-top-xl" class="g-artboard" style="">
<div style="padding: 0 0 51.2308% 0;"></div>
<div
id="g-quake-map-top-xl-img"
class="g-aiImg"
style={`background-image: url(${chartXl});`}
></div>
<div
id="g-ai4-1"
class="g-tt g-aiAbs g-aiPointText"
style="top:3.8689%;margin-top:-8.8px;left:1.5293%;width:67px;"
>
<p class="g-pstyle0">Shaking</p>
</div>
<div
id="g-ai4-2"
class="g-tt g-aiAbs g-aiPointText"
style="top:6.063%;margin-top:-7.4px;right:4.342%;width:100px;"
>
<p class="g-pstyle1">Afghanistan</p>
</div>
<div
id="g-ai4-3"
class="g-tt g-aiAbs g-aiPointText"
style="top:6.5716%;margin-top:-8.8px;left:1.5293%;width:81px;"
>
<p class="g-pstyle2">Very strong</p>
</div>
<div
id="g-ai4-4"
class="g-tt g-aiAbs g-aiPointText"
style="top:13.726%;margin-top:-8.4px;left:30.8465%;margin-left:-151px;width:302px;"
>
<p class="g-pstyle3">AFGHANISTAN</p>
</div>
<div
id="g-ai4-5"
class="g-tt g-aiAbs g-aiPointText"
style="top:15.7308%;margin-top:-8.8px;left:1.5294%;width:51px;"
>
<p class="g-pstyle2">Weak</p>
</div>
<div
id="g-ai4-6"
class="g-tt g-aiAbs g-aiPointText"
style="top:27.8288%;margin-top:-11.3px;left:43.9511%;width:67px;"
>
<p class="g-pstyle4">Gardez</p>
</div>
<div
id="g-ai4-7"
class="g-tt g-aiAbs g-aiPointText"
style="top:31.4324%;margin-top:-11.3px;left:27.588%;margin-left:-33px;width:66px;"
>
<p class="g-pstyle5">Ghazni</p>
</div>
<div
id="g-ai4-8"
class="g-tt g-aiAbs g-aiPointText"
style="top:37.1494%;margin-top:-8.4px;left:82.0619%;margin-left:-110.5px;width:221px;"
>
<p class="g-pstyle3">PAKISTAN</p>
</div>
<div
id="g-ai4-9"
class="g-tt g-aiAbs g-aiPointText"
style="top:37.8889%;margin-top:-11.3px;left:58.1285%;width:59px;"
>
<p class="g-pstyle4">Khost</p>
</div>
<div
id="g-ai4-10"
class="g-tt g-aiAbs g-aiPointText"
style="top:45.025%;margin-top:-11.9px;left:49.2661%;width:94px;"
>
<p class="g-pstyle6">Epicentre</p>
</div>
<div
id="g-ai4-11"
class="g-tt g-aiAbs g-aiPointText"
style="top:53.3543%;margin-top:-11.3px;left:70.3271%;margin-left:-32px;width:64px;"
>
<p class="g-pstyle5">Bannu</p>
</div>
</div>
{/if}
</div>
<!-- End ai2html - 2022-06-24 21:49 -->
<!-- Generated by ai2html v0.100.0 - 2022-06-24 21:49 -->
<!-- ai file: quake-map-top.ai -->
<style lang="scss">
#g-quake-map-top-box,
#g-quake-map-top-box .g-artboard {
margin: 0 auto;
}
#g-quake-map-top-box p {
margin: 0;
}
#g-quake-map-top-box .g-aiAbs {
position: absolute;
}
#g-quake-map-top-box .g-aiImg {
position: absolute;
top: 0;
display: block;
width: 100% !important;
height: 100%;
background-size: contain;
background-repeat: no-repeat;
}
#g-quake-map-top-box .g-aiPointText p {
white-space: nowrap;
}
#g-quake-map-top-xs {
position: relative;
overflow: hidden;
}
#g-quake-map-top-xs p {
font-family:
'Source Sans Pro',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
'Helvetica Neue',
Helvetica,
Arial,
sans-serif;
font-weight: 300;
line-height: 14px;
height: auto;
opacity: 1;
letter-spacing: 0em;
font-size: 12px;
text-align: left;
color: rgb(120, 119, 120);
text-transform: none;
padding-bottom: 0;
padding-top: 0;
mix-blend-mode: normal;
font-style: normal;
position: static;
}
#g-quake-map-top-xs .g-pstyle0 {
font-weight: 700;
line-height: 11px;
height: 11px;
font-size: 9px;
color: rgb(0, 0, 0);
}
#g-quake-map-top-xs .g-pstyle1 {
font-weight: 600;
height: 14px;
}
#g-quake-map-top-xs .g-pstyle2 {
height: 14px;
}
#g-quake-map-top-xs .g-pstyle3 {
line-height: 7px;
height: 7px;
letter-spacing: 0.3em;
font-size: 14px;
text-align: center;
color: rgb(94, 94, 94);
}
#g-quake-map-top-xs .g-pstyle4 {
height: 14px;
color: rgb(64, 62, 61);
}
#g-quake-map-top-xs .g-pstyle5 {
font-weight: 700;
height: 14px;
color: rgb(255, 255, 255);
}
#g-quake-map-top-xs .g-pstyle6 {
height: 14px;
text-align: center;
color: rgb(64, 62, 61);
}
#g-quake-map-top-xs .g-pstyle7 {
line-height: 7px;
height: 7px;
letter-spacing: 0.3em;
text-align: center;
color: rgb(94, 94, 94);
}
#g-quake-map-top-sm {
position: relative;
overflow: hidden;
}
#g-quake-map-top-sm p {
font-family:
'Source Sans Pro',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
'Helvetica Neue',
Helvetica,
Arial,
sans-serif;
font-weight: 300;
line-height: 7px;
height: auto;
opacity: 1;
letter-spacing: 0.4em;
font-size: 16px;
text-align: left;
color: rgb(94, 94, 94);
text-transform: none;
padding-bottom: 0;
padding-top: 0;
mix-blend-mode: normal;
font-style: normal;
position: static;
}
#g-quake-map-top-sm .g-pstyle0 {
font-weight: 600;
line-height: 16px;
height: 16px;
letter-spacing: 0em;
font-size: 14px;
color: rgb(94, 93, 92);
}
#g-quake-map-top-sm .g-pstyle1 {
line-height: 16px;
height: 16px;
letter-spacing: 0em;
font-size: 14px;
color: rgb(94, 93, 92);
}
#g-quake-map-top-sm .g-pstyle2 {
height: 7px;
text-align: center;
}
#g-quake-map-top-sm .g-pstyle3 {
line-height: 16px;
height: 16px;
letter-spacing: 0em;
font-size: 14px;
color: rgb(64, 62, 61);
}
#g-quake-map-top-sm .g-pstyle4 {
font-weight: 700;
line-height: 16px;
height: 16px;
letter-spacing: 0em;
font-size: 14px;
color: rgb(255, 255, 255);
}
#g-quake-map-top-sm .g-pstyle5 {
line-height: 16px;
height: 16px;
letter-spacing: 0em;
font-size: 14px;
text-align: center;
color: rgb(64, 62, 61);
}
#g-quake-map-top-md {
position: relative;
overflow: hidden;
}
#g-quake-map-top-md p {
font-family:
'Source Sans Pro',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
'Helvetica Neue',
Helvetica,
Arial,
sans-serif;
font-weight: 300;
line-height: 11px;
height: auto;
opacity: 1;
letter-spacing: 0.7em;
font-size: 19px;
text-align: left;
color: rgb(94, 94, 94);
text-transform: none;
padding-bottom: 0;
padding-top: 0;
mix-blend-mode: normal;
font-style: normal;
position: static;
}
#g-quake-map-top-md .g-pstyle0 {
font-weight: 600;
line-height: 14px;
height: 14px;
letter-spacing: 0em;
font-size: 12px;
color: rgb(94, 93, 92);
}
#g-quake-map-top-md .g-pstyle1 {
line-height: 14px;
height: 14px;
letter-spacing: 0em;
font-size: 12px;
color: rgb(94, 93, 92);
}
#g-quake-map-top-md .g-pstyle2 {
font-weight: 400;
height: 11px;
letter-spacing: 0.05em;
font-size: 12px;
text-align: right;
text-transform: uppercase;
color: rgb(98, 100, 100);
}
#g-quake-map-top-md .g-pstyle3 {
height: 11px;
text-align: center;
}
#g-quake-map-top-md .g-pstyle4 {
line-height: 17px;
height: 17px;
letter-spacing: 0em;
font-size: 14px;
color: rgb(64, 62, 61);
}
#g-quake-map-top-md .g-pstyle5 {
line-height: 17px;
height: 17px;
letter-spacing: 0em;
font-size: 14px;
text-align: center;
color: rgb(64, 62, 61);
}
#g-quake-map-top-md .g-pstyle6 {
font-weight: 700;
line-height: 18px;
height: 18px;
letter-spacing: 0em;
font-size: 15px;
color: rgb(255, 255, 255);
}
#g-quake-map-top-lg {
position: relative;
overflow: hidden;
}
#g-quake-map-top-lg p {
font-family:
'Source Sans Pro',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
'Helvetica Neue',
Helvetica,
Arial,
sans-serif;
font-weight: 300;
line-height: 12px;
height: auto;
opacity: 1;
letter-spacing: 0.7em;
font-size: 21px;
text-align: left;
color: rgb(94, 94, 94);
text-transform: none;
padding-bottom: 0;
padding-top: 0;
mix-blend-mode: normal;
font-style: normal;
position: static;
}
#g-quake-map-top-lg .g-pstyle0 {
font-weight: 600;
line-height: 16px;
height: 16px;
letter-spacing: 0em;
font-size: 13px;
color: rgb(94, 93, 92);
}
#g-quake-map-top-lg .g-pstyle1 {
line-height: 16px;
height: 16px;
letter-spacing: 0em;
font-size: 13px;
color: rgb(94, 93, 92);
}
#g-quake-map-top-lg .g-pstyle2 {
font-weight: 400;
height: 12px;
letter-spacing: 0.05em;
font-size: 12px;
text-align: right;
text-transform: uppercase;
color: rgb(98, 100, 100);
}
#g-quake-map-top-lg .g-pstyle3 {
height: 12px;
text-align: center;
}
#g-quake-map-top-lg .g-pstyle4 {
line-height: 19px;
height: 19px;
letter-spacing: 0em;
font-size: 16px;
color: rgb(64, 62, 61);
}
#g-quake-map-top-lg .g-pstyle5 {
line-height: 19px;
height: 19px;
letter-spacing: 0em;
font-size: 16px;
text-align: center;
color: rgb(64, 62, 61);
}
#g-quake-map-top-lg .g-pstyle6 {
font-weight: 700;
line-height: 20px;
height: 20px;
letter-spacing: 0em;
font-size: 17px;
color: rgb(255, 255, 255);
}
#g-quake-map-top-xl {
position: relative;
overflow: hidden;
}
#g-quake-map-top-xl p {
font-family:
'Source Sans Pro',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
'Helvetica Neue',
Helvetica,
Arial,
sans-serif;
font-weight: 300;
line-height: 12px;
height: auto;
opacity: 1;
letter-spacing: 0.7em;
font-size: 21px;
text-align: left;
color: rgb(94, 94, 94);
text-transform: none;
padding-bottom: 0;
padding-top: 0;
mix-blend-mode: normal;
font-style: normal;
position: static;
}
#g-quake-map-top-xl .g-pstyle0 {
font-weight: 600;
line-height: 16px;
height: 16px;
letter-spacing: 0em;
font-size: 13px;
color: rgb(94, 93, 92);
}
#g-quake-map-top-xl .g-pstyle1 {
font-weight: 400;
height: 12px;
letter-spacing: 0.05em;
font-size: 12px;
text-align: right;
text-transform: uppercase;
color: rgb(98, 100, 100);
}
#g-quake-map-top-xl .g-pstyle2 {
line-height: 16px;
height: 16px;
letter-spacing: 0em;
font-size: 13px;
color: rgb(94, 93, 92);
}
#g-quake-map-top-xl .g-pstyle3 {
height: 12px;
text-align: center;
}
#g-quake-map-top-xl .g-pstyle4 {
line-height: 19px;
height: 19px;
letter-spacing: 0em;
font-size: 16px;
color: rgb(64, 62, 61);
}
#g-quake-map-top-xl .g-pstyle5 {
line-height: 19px;
height: 19px;
letter-spacing: 0em;
font-size: 16px;
text-align: center;
color: rgb(64, 62, 61);
}
#g-quake-map-top-xl .g-pstyle6 {
font-weight: 700;
line-height: 20px;
height: 20px;
letter-spacing: 0em;
font-size: 17px;
color: rgb(255, 255, 255);
}
/* Custom CSS */
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View file

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

View file

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

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