Integrations
647
.agents/instructions/ArchieML/ARCHIEML-BETTY.md
Normal 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.
|
||||
483
.agents/instructions/graphics-components/llms.md
Normal 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.
|
||||
66
.agents/skills/svelte-code-writer/SKILL.md
Normal 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
|
|
@ -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.
|
||||
|
|
@ -1,4 +1,11 @@
|
|||
# Scratch
|
||||
## (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
|
||||
```
|
||||
|
|
@ -1,12 +1,25 @@
|
|||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
import path from 'path';
|
||||
|
||||
import svelte from '@astrojs/svelte';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
resolve: {
|
||||
alias: {
|
||||
'@components': path.resolve('./src/components'),
|
||||
}
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
loadPaths: [path.resolve('./src/styles')]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
integrations: [svelte()],
|
||||
devToolbar: {
|
||||
enabled: false
|
||||
}
|
||||
devToolbar: { enabled: false }
|
||||
});
|
||||
|
|
@ -16,7 +16,12 @@
|
|||
"@reuters-graphics/svelte-markdown": "^0.0.3",
|
||||
"@rferl/veronica": "github:rferl/veronica",
|
||||
"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-fa": "^4.0.4",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
238
pnpm-lock.yaml
|
|
@ -20,9 +20,24 @@ importers:
|
|||
astro:
|
||||
specifier: ^6.3.1
|
||||
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:
|
||||
specifier: ^5.55.5
|
||||
version: 5.55.5
|
||||
svelte-fa:
|
||||
specifier: ^4.0.4
|
||||
version: 4.0.4(svelte@5.55.5)
|
||||
typescript:
|
||||
specifier: ^5.9.3
|
||||
version: 5.9.3
|
||||
|
|
@ -418,6 +433,42 @@ packages:
|
|||
'@jridgewell/trace-mapping@0.3.31':
|
||||
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':
|
||||
resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
|
||||
|
||||
|
|
@ -725,6 +776,9 @@ packages:
|
|||
'@types/estree@1.0.9':
|
||||
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
||||
|
||||
'@types/geojson@7946.0.16':
|
||||
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
|
||||
|
||||
'@types/hast@3.0.4':
|
||||
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
|
||||
|
||||
|
|
@ -737,6 +791,9 @@ packages:
|
|||
'@types/nlcst@2.0.3':
|
||||
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':
|
||||
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
||||
|
||||
|
|
@ -915,6 +972,9 @@ packages:
|
|||
resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
earcut@3.0.2:
|
||||
resolution: {integrity: sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==}
|
||||
|
||||
entities@4.5.0:
|
||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
|
@ -976,6 +1036,9 @@ packages:
|
|||
picomatch:
|
||||
optional: true
|
||||
|
||||
fflate@0.8.2:
|
||||
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
|
||||
|
||||
flattie@1.1.1:
|
||||
resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -999,6 +1062,9 @@ packages:
|
|||
github-slugger@2.0.0:
|
||||
resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
|
||||
|
||||
gl-matrix@3.4.4:
|
||||
resolution: {integrity: sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==}
|
||||
|
||||
h3@1.15.11:
|
||||
resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==}
|
||||
|
||||
|
|
@ -1085,13 +1151,22 @@ packages:
|
|||
resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
journalize@2.6.0:
|
||||
resolution: {integrity: sha512-9Fi36vKj8MRtmqThQf8AK0GnTc1IYKCftyqwJA61dKNnkH82we2qqIPYDl1UX6farfQMCZBEV96OoEkDDQVY8g==}
|
||||
|
||||
js-yaml@4.1.1:
|
||||
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
|
||||
hasBin: true
|
||||
|
||||
json-stringify-pretty-compact@4.0.0:
|
||||
resolution: {integrity: sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==}
|
||||
|
||||
jsonc-parser@3.3.1:
|
||||
resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==}
|
||||
|
||||
kdbush@4.0.2:
|
||||
resolution: {integrity: sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==}
|
||||
|
||||
locate-character@3.0.0:
|
||||
resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
|
||||
|
||||
|
|
@ -1108,6 +1183,10 @@ packages:
|
|||
magicast@0.5.2:
|
||||
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:
|
||||
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
|
||||
|
||||
|
|
@ -1250,6 +1329,9 @@ packages:
|
|||
micromark@4.0.2:
|
||||
resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==}
|
||||
|
||||
minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
|
||||
mrmime@2.0.1:
|
||||
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -1257,6 +1339,9 @@ packages:
|
|||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
murmurhash-js@1.0.0:
|
||||
resolution: {integrity: sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==}
|
||||
|
||||
nanoid@3.3.12:
|
||||
resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
|
|
@ -1321,6 +1406,10 @@ packages:
|
|||
parse5@7.3.0:
|
||||
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
|
||||
|
||||
pbf@4.0.1:
|
||||
resolution: {integrity: sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==}
|
||||
hasBin: true
|
||||
|
||||
piccolore@0.1.3:
|
||||
resolution: {integrity: sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==}
|
||||
|
||||
|
|
@ -1335,10 +1424,16 @@ packages:
|
|||
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
pmtiles@4.4.1:
|
||||
resolution: {integrity: sha512-5oTeQc/yX/ft1evbpIlnoCZugQuug/iYIAj/ZTqIqzdGek4uZEho99En890EE6NOSI3JTI3IG8R7r8+SltphxA==}
|
||||
|
||||
postcss@8.5.14:
|
||||
resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
potpack@2.1.0:
|
||||
resolution: {integrity: sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==}
|
||||
|
||||
prismjs@1.30.0:
|
||||
resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
|
||||
engines: {node: '>=6'}
|
||||
|
|
@ -1346,6 +1441,12 @@ packages:
|
|||
property-information@7.1.0:
|
||||
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:
|
||||
resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==}
|
||||
|
||||
|
|
@ -1397,6 +1498,9 @@ packages:
|
|||
resolve-pkg-maps@1.0.0:
|
||||
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||
|
||||
resolve-protobuf-schema@2.1.0:
|
||||
resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==}
|
||||
|
||||
retext-latin@4.0.0:
|
||||
resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==}
|
||||
|
||||
|
|
@ -1562,6 +1666,10 @@ packages:
|
|||
sisteransi@1.0.5:
|
||||
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
||||
|
||||
slugify@1.6.9:
|
||||
resolution: {integrity: sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
smartypants@0.2.2:
|
||||
resolution: {integrity: sha512-TzobUYoEft/xBtb2voRPryAUIvYguG0V7Tt3de79I1WfXgCwelqVsGuZSnu3GFGRZhXR90AeEYIM+icuB/S06Q==}
|
||||
hasBin: true
|
||||
|
|
@ -1583,10 +1691,18 @@ packages:
|
|||
stringify-entities@4.0.4:
|
||||
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
|
||||
|
||||
supercluster@8.0.1:
|
||||
resolution: {integrity: sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==}
|
||||
|
||||
supports-color@8.1.1:
|
||||
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
|
||||
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:
|
||||
resolution: {integrity: sha512-JWzgeM3lqySRNfqcsesvVEh8LhTWBxQJ9RMjzJ+VepdmXtVnNd0SbtGctG6+/fbHq0N6mhwSd823gszw9JHeGQ==}
|
||||
peerDependencies:
|
||||
|
|
@ -1625,6 +1741,9 @@ packages:
|
|||
resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
tinyqueue@3.0.0:
|
||||
resolution: {integrity: sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==}
|
||||
|
||||
trim-lines@3.0.1:
|
||||
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
|
||||
|
||||
|
|
@ -2128,6 +2247,51 @@ snapshots:
|
|||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@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': {}
|
||||
|
||||
'@parcel/watcher-android-arm64@2.5.6':
|
||||
|
|
@ -2354,6 +2518,8 @@ snapshots:
|
|||
|
||||
'@types/estree@1.0.9': {}
|
||||
|
||||
'@types/geojson@7946.0.16': {}
|
||||
|
||||
'@types/hast@3.0.4':
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
|
@ -2368,6 +2534,10 @@ snapshots:
|
|||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
'@types/supercluster@7.1.3':
|
||||
dependencies:
|
||||
'@types/geojson': 7946.0.16
|
||||
|
||||
'@types/trusted-types@2.0.7': {}
|
||||
|
||||
'@types/unist@3.0.3': {}
|
||||
|
|
@ -2598,6 +2768,8 @@ snapshots:
|
|||
|
||||
dset@3.1.4: {}
|
||||
|
||||
earcut@3.0.2: {}
|
||||
|
||||
entities@4.5.0: {}
|
||||
|
||||
entities@6.0.1: {}
|
||||
|
|
@ -2663,6 +2835,8 @@ snapshots:
|
|||
optionalDependencies:
|
||||
picomatch: 4.0.4
|
||||
|
||||
fflate@0.8.2: {}
|
||||
|
||||
flattie@1.1.1: {}
|
||||
|
||||
fontace@0.4.1:
|
||||
|
|
@ -2682,6 +2856,8 @@ snapshots:
|
|||
|
||||
github-slugger@2.0.0: {}
|
||||
|
||||
gl-matrix@3.4.4: {}
|
||||
|
||||
h3@1.15.11:
|
||||
dependencies:
|
||||
cookie-es: 1.2.3
|
||||
|
|
@ -2819,12 +2995,18 @@ snapshots:
|
|||
dependencies:
|
||||
is-inside-container: 1.0.0
|
||||
|
||||
journalize@2.6.0: {}
|
||||
|
||||
js-yaml@4.1.1:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
json-stringify-pretty-compact@4.0.0: {}
|
||||
|
||||
jsonc-parser@3.3.1: {}
|
||||
|
||||
kdbush@4.0.2: {}
|
||||
|
||||
locate-character@3.0.0: {}
|
||||
|
||||
longest-streak@3.1.0: {}
|
||||
|
|
@ -2841,6 +3023,28 @@ snapshots:
|
|||
'@babel/types': 7.29.0
|
||||
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: {}
|
||||
|
||||
marked-smartypants@1.1.12(marked@15.0.12):
|
||||
|
|
@ -3165,10 +3369,14 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
mrmime@2.0.1: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
murmurhash-js@1.0.0: {}
|
||||
|
||||
nanoid@3.3.12: {}
|
||||
|
||||
neotraverse@0.6.18: {}
|
||||
|
|
@ -3234,6 +3442,10 @@ snapshots:
|
|||
dependencies:
|
||||
entities: 6.0.1
|
||||
|
||||
pbf@4.0.1:
|
||||
dependencies:
|
||||
resolve-protobuf-schema: 2.1.0
|
||||
|
||||
piccolore@0.1.3: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
|
@ -3242,16 +3454,26 @@ snapshots:
|
|||
|
||||
picomatch@4.0.4: {}
|
||||
|
||||
pmtiles@4.4.1:
|
||||
dependencies:
|
||||
fflate: 0.8.2
|
||||
|
||||
postcss@8.5.14:
|
||||
dependencies:
|
||||
nanoid: 3.3.12
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
potpack@2.1.0: {}
|
||||
|
||||
prismjs@1.30.0: {}
|
||||
|
||||
property-information@7.1.0: {}
|
||||
|
||||
protocol-buffers-schema@3.6.1: {}
|
||||
|
||||
quickselect@3.0.0: {}
|
||||
|
||||
radix3@1.1.2: {}
|
||||
|
||||
readdirp@4.1.2:
|
||||
|
|
@ -3337,6 +3559,10 @@ snapshots:
|
|||
|
||||
resolve-pkg-maps@1.0.0: {}
|
||||
|
||||
resolve-protobuf-schema@2.1.0:
|
||||
dependencies:
|
||||
protocol-buffers-schema: 3.6.1
|
||||
|
||||
retext-latin@4.0.0:
|
||||
dependencies:
|
||||
'@types/nlcst': 2.0.3
|
||||
|
|
@ -3544,6 +3770,8 @@ snapshots:
|
|||
|
||||
sisteransi@1.0.5: {}
|
||||
|
||||
slugify@1.6.9: {}
|
||||
|
||||
smartypants@0.2.2: {}
|
||||
|
||||
smol-toml@1.6.1: {}
|
||||
|
|
@ -3559,10 +3787,18 @@ snapshots:
|
|||
character-entities-html4: 2.1.0
|
||||
character-entities-legacy: 3.0.0
|
||||
|
||||
supercluster@8.0.1:
|
||||
dependencies:
|
||||
kdbush: 4.0.2
|
||||
|
||||
supports-color@8.1.1:
|
||||
dependencies:
|
||||
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):
|
||||
dependencies:
|
||||
dedent-js: 1.0.1
|
||||
|
|
@ -3618,6 +3854,8 @@ snapshots:
|
|||
fdir: 6.5.0(picomatch@4.0.4)
|
||||
picomatch: 4.0.4
|
||||
|
||||
tinyqueue@3.0.0: {}
|
||||
|
||||
trim-lines@3.0.1: {}
|
||||
|
||||
trough@2.2.0: {}
|
||||
|
|
|
|||
BIN
src/assets/Photo-4439.jpg
Executable file
|
After Width: | Height: | Size: 6.8 MiB |
BIN
src/assets/Photo-5420.jpg
Executable file
|
After Width: | Height: | Size: 5.8 MiB |
52
src/components/AdSlot/@types/ads.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
export type LeaderboardAdType = {
|
||||
mobile: {
|
||||
adType: 'leaderboard';
|
||||
placementName: 'reuters_mobile_leaderboard';
|
||||
};
|
||||
desktop: {
|
||||
adType: 'leaderboard';
|
||||
placementName: 'reuters_desktop_leaderboard_atf';
|
||||
};
|
||||
};
|
||||
export type SponsorshipAdType = {
|
||||
mobile: {
|
||||
adType: 'sponsorlogo';
|
||||
placementName: 'reuters_sponsorlogo';
|
||||
};
|
||||
desktop: {
|
||||
adType: 'sponsorlogo';
|
||||
placementName: 'reuters_sponsorlogo';
|
||||
};
|
||||
};
|
||||
export type InlineAdType = {
|
||||
mobile: {
|
||||
adType: 'mpu' | 'native' | 'mpu2';
|
||||
placementName:
|
||||
| 'reuters_mobile_mpu_1'
|
||||
| 'reuters_mobile_mpu_2'
|
||||
| 'reuters_mobile_mpu_3';
|
||||
};
|
||||
desktop: {
|
||||
adType: 'native' | 'canvas' | 'flex';
|
||||
placementName:
|
||||
| 'reuters_desktop_native_1'
|
||||
| 'reuters_desktop_native_2'
|
||||
| 'reuters_desktop_native_3';
|
||||
};
|
||||
};
|
||||
export type DesktopPlacementName =
|
||||
| LeaderboardAdType['desktop']['placementName']
|
||||
| SponsorshipAdType['desktop']['placementName']
|
||||
| InlineAdType['desktop']['placementName'];
|
||||
export type MobilePlacementName =
|
||||
| LeaderboardAdType['mobile']['placementName']
|
||||
| SponsorshipAdType['mobile']['placementName']
|
||||
| InlineAdType['mobile']['placementName'];
|
||||
export type DesktopAdType =
|
||||
| LeaderboardAdType['desktop']['adType']
|
||||
| SponsorshipAdType['desktop']['adType']
|
||||
| InlineAdType['desktop']['adType'];
|
||||
export type MobileAdType =
|
||||
| LeaderboardAdType['mobile']['adType']
|
||||
| SponsorshipAdType['mobile']['adType']
|
||||
| InlineAdType['mobile']['adType'];
|
||||
35
src/components/AdSlot/AdScripts.svelte
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { loadBootstrap } from './adScripts/bootstrap';
|
||||
import { loadScript } from './adScripts/loadScript';
|
||||
import OneTrust from './OneTrust.svelte';
|
||||
|
||||
onMount(() => {
|
||||
window.graphicsAdQueue = window.graphicsAdQueue || [];
|
||||
loadScript(
|
||||
'https://www.reuters.com/static/js/bootstrap/v1.1.2/bootstrap.static.js',
|
||||
{ onload: loadBootstrap, async: false }
|
||||
);
|
||||
// Load Freestar script
|
||||
loadScript('https://a.pub.network/reuters-com/pubfig.min.js');
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="preconnect" href="https://a.pub.network/" crossorigin="" />
|
||||
<link rel="preconnect" href="https://b.pub.network/" crossorigin="" />
|
||||
<link rel="preconnect" href="https://c.pub.network/" crossorigin="" />
|
||||
<link rel="preconnect" href="https://d.pub.network/" crossorigin="" />
|
||||
<link rel="preconnect" href="https://c.amazon-adsystem.com" crossorigin="" />
|
||||
<link rel="preconnect" href="https://s.amazon-adsystem.com" crossorigin="" />
|
||||
<link rel="preconnect" href="https://btloader.com/" crossorigin="" />
|
||||
<link rel="preconnect" href="https://api.btloader.com/" crossorigin="" />
|
||||
<link
|
||||
rel="preconnect"
|
||||
href="https://confiant-integrations.global.ssl.fastly.net"
|
||||
crossorigin=""
|
||||
/>
|
||||
<link rel="stylesheet" href="https://a.pub.network/reuters-com/cls.css" />
|
||||
</svelte:head>
|
||||
|
||||
<OneTrust />
|
||||
64
src/components/AdSlot/AdSlot.svelte
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<script lang="ts">
|
||||
import type {
|
||||
DesktopAdType,
|
||||
DesktopPlacementName,
|
||||
MobileAdType,
|
||||
MobilePlacementName,
|
||||
} from './@types/ads';
|
||||
import { onMount } from 'svelte';
|
||||
import { getRandomAdId } from './utils';
|
||||
|
||||
interface Props {
|
||||
placementName: DesktopPlacementName | MobilePlacementName;
|
||||
adType: DesktopAdType | MobileAdType;
|
||||
/**
|
||||
* @TODO Unclear at what level this bit of config is used with placements...
|
||||
*/
|
||||
dataFreestarAd?: string;
|
||||
}
|
||||
|
||||
let { placementName, adType, dataFreestarAd = '__970x250' }: Props = $props();
|
||||
|
||||
const adId = getRandomAdId();
|
||||
|
||||
onMount(() => {
|
||||
const adSlot = {
|
||||
placementName,
|
||||
slotId: adId,
|
||||
targeting: {
|
||||
div_id: placementName,
|
||||
type: adType,
|
||||
},
|
||||
};
|
||||
// @ts-ignore window global
|
||||
const freestar = window?.freestar;
|
||||
// Add adSlot to freestar queue directly if already initialised
|
||||
if (freestar) {
|
||||
freestar.queue.push(function () {
|
||||
freestar.newAdSlots([adSlot], freestar.config.channel);
|
||||
});
|
||||
// ... otherwise add to the graphicsAdQueue queue.
|
||||
} else {
|
||||
window.graphicsAdQueue = window.graphicsAdQueue || [];
|
||||
window.graphicsAdQueue.push(adSlot);
|
||||
}
|
||||
|
||||
return () => {
|
||||
// @ts-ignore window global
|
||||
const freestar = window?.freestar;
|
||||
if (freestar) {
|
||||
freestar.queue.push(function () {
|
||||
freestar.deleteAdSlots(adId);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div data-freestar-ad={dataFreestarAd || null} id={adId}></div>
|
||||
|
||||
<style>
|
||||
:global(div.freestar-adslot:has(.unfulfilled-ad)) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
56
src/components/AdSlot/InlineAd.mdx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { Meta, Canvas } from '@storybook/blocks';
|
||||
|
||||
import * as InlineAdStories from './InlineAd.stories.svelte';
|
||||
|
||||
<Meta of={InlineAdStories} />
|
||||
|
||||
# InlineAd
|
||||
|
||||
Add programmatic ads inline on your page.
|
||||
|
||||
> **IMPORTANT!** Make sure ads are only used on dotcom pages, never on embeds.
|
||||
|
||||
```svelte
|
||||
<!-- +page.svelte -->
|
||||
<script>
|
||||
import { AdScripts } from '@reuters-graphics/graphics-components';
|
||||
</script>
|
||||
|
||||
<!-- Include AdScripts only ONCE per page for any type of ad -->
|
||||
<AdScripts />
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!-- App.svelte -->
|
||||
<script>
|
||||
import { InlineAd } from '@reuters-graphics/graphics-components';
|
||||
|
||||
let { embedded = false } = $props();
|
||||
</script>
|
||||
|
||||
{#each content.blocks as block}
|
||||
<!-- ... -->
|
||||
|
||||
{#if block.Type === 'inline-ad'}
|
||||
<!-- Check if in an embed context! -->
|
||||
{#if !embedded}
|
||||
<InlineAd />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- ... -->
|
||||
{/each}
|
||||
```
|
||||
|
||||
You may add **up to three** inline ads per page, but must set the `n` prop on multiple ads in sequential order, 1 - 3.
|
||||
|
||||
```svelte
|
||||
<!-- First inline ad on the page -->
|
||||
<InlineAd n={1} />
|
||||
<!-- ... second ... -->
|
||||
<InlineAd n={2} />
|
||||
<!-- ... third and final. -->
|
||||
<InlineAd n={3} />
|
||||
```
|
||||
|
||||
<Canvas of={InlineAdStories.Demo} />
|
||||
20
src/components/AdSlot/InlineAd.stories.svelte
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<script module lang="ts">
|
||||
import AdScripts from './AdScripts.svelte';
|
||||
import InlineAd from './InlineAd.svelte';
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Components/Ads & analytics/InlineAd',
|
||||
component: InlineAd,
|
||||
});
|
||||
</script>
|
||||
|
||||
{#snippet template()}
|
||||
<div>
|
||||
<AdScripts />
|
||||
<InlineAd />
|
||||
<InlineAd />
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<Story name="Demo" children={template} />
|
||||
74
src/components/AdSlot/InlineAd.svelte
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<!-- @migration-task Error while migrating Svelte code: Cannot set properties of undefined (setting 'next') -->
|
||||
<!-- @component `InlineAd` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-ads-analytics-inlinead--docs) -->
|
||||
<script lang="ts">
|
||||
import Block from '../Block/Block.svelte';
|
||||
import type { InlineAdType } from './@types/ads';
|
||||
import ResponsiveAd from './ResponsiveAd.svelte';
|
||||
|
||||
interface Props {
|
||||
/** Add an ID to target with SCSS. */
|
||||
id?: string;
|
||||
/** Number of the inline ad in sequence. Use to add multiple inline ads to a page. */
|
||||
n?: 1 | 2 | 3 | '1' | '2' | '3';
|
||||
/** Add a class to target with SCSS. Defaults to `my-12`. */
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let { id = '', class: cls = 'my-12', n = 1 }: Props = $props();
|
||||
|
||||
const desktopPlacementName: InlineAdType['desktop']['placementName'] = `reuters_desktop_native_${n}`;
|
||||
</script>
|
||||
|
||||
<Block {id} class="freestar-adslot {cls}">
|
||||
<div class="ad-block">
|
||||
<div class="ad-label">Advertisement · Scroll to continue</div>
|
||||
<div class="ad-container">
|
||||
<div class="ad-slot__inner">
|
||||
<div>
|
||||
<ResponsiveAd {desktopPlacementName} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Block>
|
||||
|
||||
<style lang="scss">
|
||||
div.ad-block {
|
||||
border-bottom: 1px solid var(--theme-colour-brand-rules);
|
||||
border-top: 1px solid var(--theme-colour-brand-rules);
|
||||
div.ad-label {
|
||||
font-family: Knowledge, 'Source Sans Pro', Arial, Helvetica, sans-serif;
|
||||
font-size: 14px;
|
||||
margin: 6px 0;
|
||||
line-height: 1.333;
|
||||
color: var(--theme-colour-text-secondary);
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
div.ad-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 415px;
|
||||
@media (max-width: 767.9px) {
|
||||
min-height: 320px;
|
||||
}
|
||||
div.ad-slot__inner {
|
||||
margin: auto 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
flex: unset;
|
||||
& > div {
|
||||
display: block;
|
||||
:global(div[data-freestar-ad]) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
31
src/components/AdSlot/LeaderboardAd.mdx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { Meta, Canvas } from '@storybook/blocks';
|
||||
|
||||
import * as LeaderboardAdStories from './LeaderboardAd.stories.svelte';
|
||||
|
||||
<Meta of={LeaderboardAdStories} />
|
||||
|
||||
# LeaderboardAd
|
||||
|
||||
Add a leaderboard ad to your page.
|
||||
|
||||
> **IMPORTANT!** Make sure ads are only used on dotcom pages, never on embeds.
|
||||
|
||||
```svelte
|
||||
<!-- +page.svelte -->
|
||||
<script>
|
||||
import {
|
||||
AdScripts,
|
||||
LeaderboardAd,
|
||||
SiteHeader,
|
||||
} from '@reuters-graphics/graphics-components';
|
||||
</script>
|
||||
|
||||
<!-- Include AdScripts only ONCE per page for any type of ad -->
|
||||
<AdScripts />
|
||||
|
||||
<!-- ALWAYS put the leaderboard ad directly above the SiteHeader -->
|
||||
<LeaderboardAd />
|
||||
<SiteHeader />
|
||||
```
|
||||
|
||||
<Canvas of={LeaderboardAdStories.Demo} />
|
||||
29
src/components/AdSlot/LeaderboardAd.stories.svelte
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<script module lang="ts">
|
||||
import AdScripts from './AdScripts.svelte';
|
||||
import LeaderboardAd from './LeaderboardAd.svelte';
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Components/Ads & analytics/LeaderboardAd',
|
||||
component: LeaderboardAd,
|
||||
});
|
||||
</script>
|
||||
|
||||
{#snippet template()}
|
||||
<div>
|
||||
<AdScripts />
|
||||
<LeaderboardAd />
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<Story name="Demo" children={template} />
|
||||
|
||||
<style>
|
||||
div {
|
||||
min-height: 200vh;
|
||||
background-size: 40px 40px;
|
||||
background-image:
|
||||
linear-gradient(to right, lightgrey 1px, transparent 1px),
|
||||
linear-gradient(to bottom, lightgrey 1px, transparent 1px);
|
||||
}
|
||||
</style>
|
||||
102
src/components/AdSlot/LeaderboardAd.svelte
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
<!-- @component `LeaderboardAd` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-ads-analytics-leaderboardad--docs) -->
|
||||
<script lang="ts">
|
||||
import type { LeaderboardAdType } from './@types/ads';
|
||||
import ResponsiveAd from './ResponsiveAd.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
/** Add an ID to target with SCSS. */
|
||||
id?: string;
|
||||
/** Add a class to target with SCSS. */
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let { id = '', class: cls = '' }: Props = $props();
|
||||
|
||||
let windowWidth = $state(1200);
|
||||
let adSize = $derived(windowWidth < 1024 ? 110 : 275);
|
||||
|
||||
const desktopPlacementName: LeaderboardAdType['desktop']['placementName'] =
|
||||
'reuters_desktop_leaderboard_atf';
|
||||
|
||||
let sticky = $state(false);
|
||||
// Handles transition out... somewhat dumb, but here we are...
|
||||
let unstick = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
const handleScroll = () => {
|
||||
const scrollTop = window.scrollY;
|
||||
if (scrollTop >= adSize * 1.1) {
|
||||
sticky = true;
|
||||
setTimeout(() => {
|
||||
unstick = true;
|
||||
setTimeout(() => {
|
||||
sticky = false;
|
||||
}, 400);
|
||||
}, 1500);
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerWidth={windowWidth} />
|
||||
|
||||
<div
|
||||
class="freestar-adslot leaderboard__sticky {cls}"
|
||||
class:sticky
|
||||
class:unstick
|
||||
{id}
|
||||
style="--height: {adSize}px;"
|
||||
>
|
||||
<div class="ad-block">
|
||||
<div class="ad-slot__container">
|
||||
<div class="ad-slot__inner">
|
||||
<div>
|
||||
<ResponsiveAd {desktopPlacementName} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.leaderboard__sticky {
|
||||
position: initial;
|
||||
top: -275px;
|
||||
transition: top 0.4s ease-in-out;
|
||||
z-index: 1030;
|
||||
&.sticky {
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
}
|
||||
&.unstick {
|
||||
top: -275px;
|
||||
}
|
||||
}
|
||||
div.ad-block {
|
||||
width: 100%;
|
||||
background: #f4f4f4;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: var(--height);
|
||||
.ad-slot__container {
|
||||
height: 0px;
|
||||
min-height: var(--height);
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
.ad-slot__inner {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
44
src/components/AdSlot/OneTrust.svelte
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<!-- This component manages the OneTrust prefs button, so it's not permanently fixed on page... -->
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { throttle } from 'es-toolkit';
|
||||
|
||||
let lastScroll = 0;
|
||||
let showManagePreferences = true;
|
||||
|
||||
const togglePrefs = (on: boolean = true) => {
|
||||
const btn = document.getElementById('ot-sdk-btn-floating');
|
||||
if (!btn) return;
|
||||
if (on) {
|
||||
showManagePreferences = true;
|
||||
btn.style.bottom = '';
|
||||
} else {
|
||||
showManagePreferences = false;
|
||||
btn.style.bottom = '-5rem';
|
||||
}
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
if (lastScroll > window.scrollY) {
|
||||
if (!showManagePreferences) {
|
||||
togglePrefs(true);
|
||||
}
|
||||
} else {
|
||||
if (showManagePreferences && window.scrollY > 250) {
|
||||
togglePrefs(false);
|
||||
}
|
||||
}
|
||||
lastScroll = window.scrollY;
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
const throttledHandle = throttle(handleScroll, 250);
|
||||
window.addEventListener('scroll', throttledHandle, {
|
||||
passive: true,
|
||||
});
|
||||
return () => {
|
||||
window.removeEventListener('scroll', throttledHandle);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
78
src/components/AdSlot/ResponsiveAd.svelte
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
<script lang="ts">
|
||||
import type {
|
||||
DesktopAdType,
|
||||
DesktopPlacementName,
|
||||
MobileAdType,
|
||||
MobilePlacementName,
|
||||
} from './@types/ads';
|
||||
import AdSlot from './AdSlot.svelte';
|
||||
|
||||
interface Props {
|
||||
desktopPlacementName: DesktopPlacementName;
|
||||
mobileBreakpoint?: number;
|
||||
}
|
||||
|
||||
let { desktopPlacementName, mobileBreakpoint = 1024 }: Props = $props();
|
||||
|
||||
let windowWidth: number | undefined = $state();
|
||||
|
||||
const getMobilePlacementName = (
|
||||
desktopPlacementName: DesktopPlacementName
|
||||
): MobilePlacementName => {
|
||||
switch (desktopPlacementName) {
|
||||
case 'reuters_desktop_leaderboard_atf':
|
||||
return 'reuters_mobile_leaderboard';
|
||||
case 'reuters_sponsorlogo':
|
||||
return 'reuters_sponsorlogo';
|
||||
case 'reuters_desktop_native_1':
|
||||
return 'reuters_mobile_mpu_1';
|
||||
case 'reuters_desktop_native_2':
|
||||
return 'reuters_mobile_mpu_2';
|
||||
case 'reuters_desktop_native_3':
|
||||
return 'reuters_mobile_mpu_3';
|
||||
default:
|
||||
return 'reuters_mobile_mpu_1';
|
||||
}
|
||||
};
|
||||
|
||||
const getAdType = (
|
||||
placementName: DesktopPlacementName | MobilePlacementName
|
||||
): DesktopAdType | MobileAdType => {
|
||||
switch (placementName) {
|
||||
case 'reuters_desktop_leaderboard_atf':
|
||||
case 'reuters_mobile_leaderboard':
|
||||
return 'leaderboard';
|
||||
case 'reuters_sponsorlogo':
|
||||
return 'sponsorlogo';
|
||||
case 'reuters_mobile_mpu_1':
|
||||
return 'mpu';
|
||||
case 'reuters_mobile_mpu_2':
|
||||
return 'native';
|
||||
case 'reuters_mobile_mpu_3':
|
||||
return 'mpu2';
|
||||
case 'reuters_desktop_native_1':
|
||||
return 'native';
|
||||
case 'reuters_desktop_native_2':
|
||||
return 'canvas';
|
||||
case 'reuters_desktop_native_3':
|
||||
return 'flex';
|
||||
default:
|
||||
return 'native';
|
||||
}
|
||||
};
|
||||
|
||||
let placementName = $derived(
|
||||
windowWidth && windowWidth < mobileBreakpoint ?
|
||||
getMobilePlacementName(desktopPlacementName)
|
||||
: desktopPlacementName
|
||||
);
|
||||
let adType = $derived(getAdType(placementName));
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerWidth={windowWidth} />
|
||||
|
||||
{#if windowWidth}
|
||||
{#key placementName}
|
||||
<AdSlot {placementName} {adType} />
|
||||
{/key}
|
||||
{/if}
|
||||
37
src/components/AdSlot/SponsorshipAd.mdx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { Meta, Canvas } from '@storybook/blocks';
|
||||
|
||||
import * as SponsorshipAdStories from './SponsorshipAd.stories.svelte';
|
||||
|
||||
<Meta of={SponsorshipAdStories} />
|
||||
|
||||
# SponsorshipAd
|
||||
|
||||
Add a sponsorship ad to your page.
|
||||
|
||||
> **IMPORTANT!** Make sure ads are only used on dotcom pages, never on embeds.
|
||||
|
||||
```svelte
|
||||
<!-- +page.svelte -->
|
||||
<script>
|
||||
import { AdScripts } from '@reuters-graphics/graphics-components';
|
||||
</script>
|
||||
|
||||
<!-- Include AdScripts only ONCE per page for any type of ad -->
|
||||
<AdScripts />
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!-- App.svelte -->
|
||||
<script>
|
||||
import { SponsorshipAd } from '@reuters-graphics/graphics-components';
|
||||
|
||||
let { embedded = false } = $props();
|
||||
</script>
|
||||
|
||||
<!-- Check if in an embed context! -->
|
||||
{#if !embedded}
|
||||
<SponsorshipAd />
|
||||
{/if}
|
||||
```
|
||||
|
||||
<Canvas of={SponsorshipAdStories.Demo} />
|
||||
20
src/components/AdSlot/SponsorshipAd.stories.svelte
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
|
||||
import AdScripts from './AdScripts.svelte';
|
||||
import SponsorshipAd from './SponsorshipAd.svelte';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Components/Ads & analytics/SponsorshipAd',
|
||||
component: SponsorshipAd,
|
||||
});
|
||||
</script>
|
||||
|
||||
{#snippet template()}
|
||||
<div>
|
||||
<AdScripts />
|
||||
<SponsorshipAd />
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<Story name="Demo" children={template} />
|
||||
85
src/components/AdSlot/SponsorshipAd.svelte
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<!-- @migration-task Error while migrating Svelte code: Cannot set properties of undefined (setting 'next') -->
|
||||
<!-- @component `SponsorshipAd` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-ads-analytics-sponsorshipad--docs) -->
|
||||
<script lang="ts">
|
||||
import Block from '../Block/Block.svelte';
|
||||
import type { SponsorshipAdType } from './@types/ads';
|
||||
import ResponsiveAd from './ResponsiveAd.svelte';
|
||||
|
||||
interface Props {
|
||||
/** Add an ID to target with SCSS. */
|
||||
id?: string;
|
||||
/** Add a class to target with SCSS. */
|
||||
class?: string;
|
||||
/**
|
||||
* Label placed directly above the sponsorship ad
|
||||
*/
|
||||
adLabel?: string;
|
||||
}
|
||||
|
||||
let { id = '', class: cls = 'my-12', adLabel = '' }: Props = $props();
|
||||
|
||||
const desktopPlacementName: SponsorshipAdType['desktop']['placementName'] =
|
||||
'reuters_sponsorlogo';
|
||||
</script>
|
||||
|
||||
<Block {id} class="freestar-adslot {cls}">
|
||||
<div class="ad-block">
|
||||
{#if adLabel}
|
||||
<div class="ad-label">
|
||||
<div>{adLabel}</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="ad-container">
|
||||
<div class="ad-slot__inner">
|
||||
<div>
|
||||
<ResponsiveAd {desktopPlacementName} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Block>
|
||||
|
||||
<style lang="scss">
|
||||
div.ad-block {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
div.ad-label {
|
||||
font-family: Knowledge, 'Source Sans Pro', Arial, Helvetica, sans-serif;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
line-height: 1.333;
|
||||
color: var(--theme-colour-text-secondary);
|
||||
text-align: right;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-items: center;
|
||||
}
|
||||
div.ad-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 32px;
|
||||
div.ad-slot__inner {
|
||||
margin: auto 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
flex: unset;
|
||||
& > div {
|
||||
display: block;
|
||||
:global(div[data-freestar-ad]) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
105
src/components/AdSlot/adScripts/bootstrap.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import getParameterByName from './getParameterByName';
|
||||
import Ias from './ias';
|
||||
|
||||
const ONETRUST_LOGS = 'ot_logs';
|
||||
const ONETRUST_GEOLOCATION_MOCK = 'ot_geolocation_mock';
|
||||
const ONETRUST_SCRIPT_ID = '38cb75bd-fbe1-4ac8-b4af-e531ab368caf';
|
||||
|
||||
export const loadBootstrap = () => {
|
||||
(<any>window).freestar = (<any>window).freestar || {};
|
||||
const freestar = (<any>window).freestar;
|
||||
freestar.debug = true;
|
||||
freestar.queue = freestar.queue || [];
|
||||
freestar.config = freestar.config || {};
|
||||
freestar.config.enabled_slots = [];
|
||||
freestar.initCallback = function () {
|
||||
if (freestar.config.enabled_slots.length === 0) {
|
||||
freestar.initCallbackCalled = false;
|
||||
} else {
|
||||
freestar.newAdSlots(freestar.config.enabled_slots);
|
||||
}
|
||||
};
|
||||
|
||||
freestar.config.channel = '/4735792/reuters.com/graphics';
|
||||
|
||||
(<any>window).initBootstrap(
|
||||
{
|
||||
onetrust_logs: getParameterByName(ONETRUST_LOGS) || 'false',
|
||||
geolocation_mock:
|
||||
getParameterByName(ONETRUST_GEOLOCATION_MOCK) || 'default',
|
||||
onetrust_script_id: ONETRUST_SCRIPT_ID,
|
||||
},
|
||||
(onetrustResponse: any) => {
|
||||
const iasPromise = Ias();
|
||||
return Promise.all([iasPromise]).then((responses) => {
|
||||
const [iasResponse] = responses;
|
||||
|
||||
return {
|
||||
...onetrustResponse,
|
||||
ias: iasResponse,
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
(<any>window).bootstrap.getResults(() => {
|
||||
// Set GAM
|
||||
window.googletag = (<any>window).googletag || { cmd: [] };
|
||||
window.googletag.cmd.push(() => {
|
||||
window.googletag.pubads().enableSingleRequest();
|
||||
/**
|
||||
* @TODO Property 'enableAsyncRendering' does not exist on type 'PubAdsService'.
|
||||
*/
|
||||
// @ts-ignore window global
|
||||
window.googletag.pubads().enableAsyncRendering();
|
||||
window.googletag.pubads().collapseEmptyDivs(true);
|
||||
|
||||
window.googletag
|
||||
.pubads()
|
||||
.addEventListener('slotRenderEnded', function (event) {
|
||||
const adDiv = document.getElementById(event.slot.getSlotElementId());
|
||||
if (!adDiv) return;
|
||||
// If the ad slot is empty
|
||||
if (event.isEmpty) {
|
||||
adDiv.classList.add('unfulfilled-ad');
|
||||
} else {
|
||||
adDiv.classList.remove('unfulfilled-ad');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Set page-level key-values
|
||||
// cf: https://help.freestar.com/help/using-key-values
|
||||
freestar.queue.push(function () {
|
||||
// Global Ads test targeting
|
||||
const adstest = new URL(document.location.href).searchParams.get(
|
||||
'adstest'
|
||||
);
|
||||
if (adstest) {
|
||||
window.googletag.pubads().setTargeting('adstest', adstest);
|
||||
}
|
||||
|
||||
// Use the URL path to create a unique ID for the page.
|
||||
const graphicId =
|
||||
window.location.pathname
|
||||
.split('/')
|
||||
// Get the first lowercase slug in the pathname, which is the graphic UID.
|
||||
.filter((d) => d.match(/[a-z0-9]+/) && d !== 'graphics')[0] ||
|
||||
'unknown-graphic';
|
||||
window.googletag.pubads().setTargeting('template', 'graphics');
|
||||
window.googletag.pubads().setTargeting('graphicId', graphicId);
|
||||
});
|
||||
|
||||
if (!Array.isArray((<any>window).graphicsAdQueue)) {
|
||||
console.error('Ad queue not initialized!');
|
||||
}
|
||||
|
||||
freestar.queue.push(function () {
|
||||
freestar.newAdSlots(
|
||||
(<any>window).graphicsAdQueue || [],
|
||||
freestar.config.channel
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
12
src/components/AdSlot/adScripts/getParameterByName.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export default (name: string, url = window.location.href) => {
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
name = name.replace(/[\[\]]/g, '\\$&');
|
||||
const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)');
|
||||
const results = regex.exec(url);
|
||||
|
||||
if (!results) return null;
|
||||
|
||||
if (!results[2]) return '';
|
||||
|
||||
return decodeURIComponent(results[2].replace(/\+/g, ' '));
|
||||
};
|
||||
28
src/components/AdSlot/adScripts/ias.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
const IAS_REQUEST_TIMEOUT = 600;
|
||||
|
||||
export default () => {
|
||||
return new Promise((resolve) => {
|
||||
const timerId = setTimeout(() => {
|
||||
resolve('Resolved with timeout');
|
||||
}, IAS_REQUEST_TIMEOUT);
|
||||
|
||||
const setupIAS = () => {
|
||||
clearTimeout(timerId);
|
||||
(<any>window).__iasPET = (<any>window).__iasPET || {};
|
||||
(<any>window).__iasPET.queue = (<any>window).__iasPET.queue || [];
|
||||
(<any>window).__iasPET.pubId = '931336'; // Ask Rachel
|
||||
resolve('loaded');
|
||||
};
|
||||
|
||||
// Set up IAS pet.js
|
||||
const script = document.createElement('script');
|
||||
script.src = '//static.adsafeprotected.com/iasPET.1.js';
|
||||
script.setAttribute('async', 'async');
|
||||
document.head.appendChild(script);
|
||||
script.onload = setupIAS;
|
||||
script.onerror = () => {
|
||||
resolve('error');
|
||||
};
|
||||
});
|
||||
};
|
||||
17
src/components/AdSlot/adScripts/loadScript.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
interface attributesInterface {
|
||||
onload?: () => void;
|
||||
async?: boolean;
|
||||
}
|
||||
|
||||
export const loadScript = (src: string, attributes?: attributesInterface) => {
|
||||
const { onload, async = true } = attributes || {};
|
||||
|
||||
const existingScript = document.querySelector(`script[src="${src}"]`);
|
||||
if (existingScript) return;
|
||||
|
||||
const script = document.createElement('script');
|
||||
if (onload) script.addEventListener('load', onload);
|
||||
script.async = async;
|
||||
script.src = src;
|
||||
document.head.append(script);
|
||||
};
|
||||
6
src/components/AdSlot/utils.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
const random4 = () =>
|
||||
Math.floor((1 + Math.random()) * 0x10000)
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
|
||||
export const getRandomAdId = () => 'ad-' + random4() + random4();
|
||||
73
src/components/Analytics/Analytics.mdx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { Meta } from '@storybook/blocks';
|
||||
|
||||
import * as AnalyticsStories from './Analytics.stories.svelte';
|
||||
|
||||
<Meta of={AnalyticsStories} />
|
||||
|
||||
# Analytics
|
||||
|
||||
The `Analytics` component adds Google and Chartbeat analytics to your page.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { Analytics } from '@reuters-graphics/graphics-components';
|
||||
|
||||
const authors = [{ name: 'Jane Doe' }, { name: 'John Doe' }];
|
||||
</script>
|
||||
|
||||
<Analytics {authors} />
|
||||
```
|
||||
|
||||
## Environments
|
||||
|
||||
Generally, you only want to send page analytics in production environments.
|
||||
|
||||
In a SvelteKit context, you can use `$app` stores to restrict when you send analytics.
|
||||
|
||||
For example, the following excludes analytics from pages in development or hosted on our preview server:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { Analytics } from '@reuters-graphics/graphics-components';
|
||||
import { dev } from '$app/environment';
|
||||
import { page } from '$app/stores';
|
||||
</script>
|
||||
|
||||
{#if !dev && $page.url?.hostname !== 'graphics.thomsonreuters.com'}
|
||||
<Analytics />
|
||||
{/if}
|
||||
```
|
||||
|
||||
## Multipage apps
|
||||
|
||||
If you're using analytics to measure a multipage newsapp that uses [client-side routing](https://kit.svelte.dev/docs/glossary#routing), then you may need to trigger analytics after virtual page navigation.
|
||||
|
||||
This component exports a function you can call to register pageviews.
|
||||
|
||||
For example, here's how you can use SvelteKit's [`afterNavigate`](https://kit.svelte.dev/docs/modules#$app-navigation-afternavigate) lifecycle to capture additional pageviews:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import {
|
||||
Analytics,
|
||||
registerPageview,
|
||||
} from '@reuters-graphics/graphics-components';
|
||||
import { afterNavigate } from '$app/navigation';
|
||||
|
||||
let isFirstPageview = true;
|
||||
|
||||
afterNavigate(() => {
|
||||
// We shouldn't fire on initial page load because the Analytics component
|
||||
// already registers a reader's first pageview. After this component
|
||||
// has initially mounted, we can be sure that further navigation is virtual
|
||||
// and register pageviews using this function.
|
||||
if (!isFirstPageview) {
|
||||
registerPageview();
|
||||
} else {
|
||||
isFirstPageview = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Analytics />
|
||||
```
|
||||
17
src/components/Analytics/Analytics.stories.svelte
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import Analytics from './Analytics.svelte';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Components/Ads & analytics/Analytics',
|
||||
component: Analytics,
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story
|
||||
name="Demo"
|
||||
tags={['!autodocs', '!dev']}
|
||||
args={{
|
||||
authors: [{ name: 'Jane Doe' }, { name: 'John Doe' }],
|
||||
}}
|
||||
/>
|
||||
39
src/components/Analytics/Analytics.svelte
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<!-- @component `Analytics` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-ads-analytics-analytics--docs) -->
|
||||
<script module>
|
||||
import { registerPageview as registerChartbeatPageview } from './providers/chartbeat';
|
||||
import { registerPageview as registerGAPageview } from './providers/ga';
|
||||
|
||||
/** Register virtual pageviews when using client-side routing in multipage applications. */
|
||||
function registerPageview() {
|
||||
registerChartbeatPageview();
|
||||
registerGAPageview();
|
||||
}
|
||||
|
||||
export { registerPageview };
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
interface Author {
|
||||
name: string;
|
||||
}
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
import { ga, chartbeat } from './providers';
|
||||
import GoogleTagManager from './GTM.svelte';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* Used to associate a page with its author(s) in Chartbeat.
|
||||
*/
|
||||
authors?: Author[];
|
||||
}
|
||||
|
||||
let { authors = [] }: Props = $props();
|
||||
|
||||
onMount(() => {
|
||||
ga();
|
||||
chartbeat(authors);
|
||||
});
|
||||
</script>
|
||||
|
||||
<GoogleTagManager />
|
||||
30
src/components/Analytics/GTM.svelte
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<script lang="ts">
|
||||
const GTM_ID = 'GTM-P9TTSWG2';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<!-- Google Tag Manager -->
|
||||
<link href="https://www.googletagmanager.com" rel="preconnect" />
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
window.dataLayer.push({
|
||||
'gtm.start': new Date().getTime(),
|
||||
event: 'gtm.js',
|
||||
});
|
||||
</script>
|
||||
<script async src={`https://www.googletagmanager.com/gtm.js?id=${GTM_ID}`}>
|
||||
</script>
|
||||
<!-- End Google Tag Manager -->
|
||||
</svelte:head>
|
||||
|
||||
<!-- Google Tag Manager (noscript) -->
|
||||
<noscript>
|
||||
<iframe
|
||||
src={`https://www.googletagmanager.com/ns.html?id=${GTM_ID}`}
|
||||
height="0"
|
||||
width="0"
|
||||
style="display:none;visibility:hidden"
|
||||
title=""
|
||||
></iframe>
|
||||
</noscript>
|
||||
<!-- End Google Tag Manager (noscript) -->
|
||||
23
src/components/Analytics/providers/chartbeat.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// Reuters Chartbeat UID
|
||||
const UID = 52639;
|
||||
|
||||
export default (authors: { name: string }[]) => {
|
||||
window._sf_async_config = {
|
||||
uid: UID,
|
||||
domain: 'reuters.com',
|
||||
flickerControl: false,
|
||||
useCanonical: true,
|
||||
useCanonicalDomain: true,
|
||||
sections: 'Graphics',
|
||||
authors: authors.map((a) => a?.name || '').join(','),
|
||||
...(window._sf_async_config || {}),
|
||||
};
|
||||
};
|
||||
|
||||
export const registerPageview = () => {
|
||||
if (typeof window === 'undefined' || !window.pSUPERFLY) return;
|
||||
window.pSUPERFLY.virtualPage({
|
||||
path: window.location.pathname,
|
||||
title: document?.title,
|
||||
});
|
||||
};
|
||||
24
src/components/Analytics/providers/ga.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
export default () => {
|
||||
try {
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
if (!window.gtag) {
|
||||
/** @type {Gtag.Gtag} */
|
||||
window.gtag = function () {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
window.dataLayer.push(arguments);
|
||||
};
|
||||
window.gtag('js', new Date());
|
||||
registerPageview();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Error initialising Google Analytics: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const registerPageview = () => {
|
||||
if (typeof window === 'undefined' || !window.gtag) return;
|
||||
window.gtag('event', 'page_view', {
|
||||
page_location: window.location.origin + window.location.pathname,
|
||||
page_title: document?.title,
|
||||
});
|
||||
};
|
||||
2
src/components/Analytics/providers/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { default as ga } from './ga';
|
||||
export { default as chartbeat } from './chartbeat';
|
||||
111
src/components/BeforeAfter/BeforeAfter.mdx
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import { Meta, Canvas } from '@storybook/blocks';
|
||||
|
||||
import * as BeforeAfterStories from './BeforeAfter.stories.svelte';
|
||||
|
||||
<Meta of={BeforeAfterStories} />
|
||||
|
||||
# BeforeAfter
|
||||
|
||||
The `BeforeAfter` component shows a before-and-after comparison of an image.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { BeforeAfter } from '@reuters-graphics/graphics-components';
|
||||
import { assets } from '$app/paths'; // 👈 If using in the graphics kit...
|
||||
</script>
|
||||
|
||||
<BeforeAfter
|
||||
beforeSrc={`${assets}/images/before-after/myrne-before.jpg`}
|
||||
beforeAlt="Satellite image of Russian base at Myrne taken on July 7, 2020."
|
||||
afterSrc={`${assets}/images/before-after/myrne-after.jpg`}
|
||||
afterAlt="Satellite image of Russian base at Myrne taken on Oct. 20, 2020."
|
||||
/>
|
||||
```
|
||||
|
||||
<Canvas of={BeforeAfterStories.Demo} />
|
||||
|
||||
## Using with ArchieML docs
|
||||
|
||||
With the graphics kit, you'll likely get your text value from an ArchieML doc...
|
||||
|
||||
```yaml
|
||||
# ArchieML doc
|
||||
[blocks]
|
||||
|
||||
type: before-after
|
||||
beforeSrc: images/before-after/myrne-before.jpg
|
||||
beforeAlt: Satellite image of Russian base at Myrne taken on July 7, 2020.
|
||||
afterSrc: images/before-after/myrne-after.jpg
|
||||
afterAlt: Satellite image of Russian base at Myrne taken on Oct. 20, 2020.
|
||||
|
||||
[]
|
||||
```
|
||||
|
||||
... which you'll parse out of a ArchieML block object before passing to the `BeforeAfter` component.
|
||||
|
||||
```svelte
|
||||
<!-- App.svelte -->
|
||||
{#each content.blocks as block}
|
||||
{#if block.type === 'before-after'}
|
||||
<BeforeAfter
|
||||
beforeSrc={`${assets}/${block.beforeSrc}`}
|
||||
beforeAlt={block.beforeAlt}
|
||||
afterSrc={`${assets}/${block.afterSrc}`}
|
||||
afterAlt={block.afterAlt}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
```
|
||||
|
||||
<Canvas of={BeforeAfterStories.Demo} />
|
||||
|
||||
## Adding text
|
||||
|
||||
To add text overlays and captions, use [snippets](https://svelte.dev/docs/svelte/snippet) for `beforeOverlay`, `afterOverlay` and `caption`. You can style the snippets to match your page design, like in [this demo](./?path=/story/components-multimedia-beforeafter--with-overlays).
|
||||
|
||||
> 💡**NOTE:** The text in the overlays are used as [ARIA descriptions](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-describedby) for your before and after images. You must always use the `beforeAlt` / `afterAlt` props to label your image for visually impaired readers, but these ARIA descriptions provide additional information or context that the reader might need.
|
||||
|
||||
```svelte
|
||||
<BeforeAfter
|
||||
beforeSrc={beforeImg}
|
||||
beforeAlt="Satellite image of Russian base at Myrne taken on July 7, 2020."
|
||||
afterSrc={afterImg}
|
||||
afterAlt="Satellite image of Russian base at Myrne taken on Oct. 20, 2020."
|
||||
>
|
||||
<!-- Optional custom text overlay for the before image -->
|
||||
{#snippet beforeOverlay()}
|
||||
<div class="overlay p-3 before text-left">
|
||||
<p class="h4 font-bold">July 7, 2020</p>
|
||||
<p class="body-note">Initially, this site was far smaller.</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<!-- Optional custom text overlay for the after image -->
|
||||
{#snippet afterOverlay()}
|
||||
<div class="overlay p-3 after text-right">
|
||||
<p class="h4 font-bold">Oct. 20, 2020</p>
|
||||
<p class="body-note">But then forces built up.</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<!-- Optional custom caption for both images -->
|
||||
{#snippet caption()}
|
||||
<p class="body-note">Photos by MAXAR Technologies, 2021.</p>
|
||||
{/snippet}
|
||||
</BeforeAfter>
|
||||
|
||||
<style lang="scss">
|
||||
.overlay {
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
max-width: 350px;
|
||||
&.after {
|
||||
text-align: right;
|
||||
}
|
||||
p {
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
<Canvas of={BeforeAfterStories.WithText} />
|
||||
68
src/components/BeforeAfter/BeforeAfter.stories.svelte
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import BeforeAfter from './BeforeAfter.svelte';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Components/Multimedia/BeforeAfter',
|
||||
component: BeforeAfter,
|
||||
argTypes: {
|
||||
handleColour: { control: 'color' },
|
||||
width: {
|
||||
control: 'select',
|
||||
options: ['normal', 'wide', 'wider', 'widest', 'fluid'],
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import beforeImg from './images/myrne-before.jpg';
|
||||
import afterImg from './images/myrne-after.jpg';
|
||||
</script>
|
||||
|
||||
<Story
|
||||
name="Demo"
|
||||
args={{
|
||||
beforeSrc: beforeImg,
|
||||
beforeAlt:
|
||||
'Satellite image of Russian base at Myrne taken on July 7, 2020.',
|
||||
afterSrc: afterImg,
|
||||
afterAlt:
|
||||
'Satellite image of Russian base at Myrne taken on Oct. 20, 2020.',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Story name="With text" exportName="WithText">
|
||||
<BeforeAfter
|
||||
beforeSrc={beforeImg}
|
||||
beforeAlt="Satellite image of Russian base at Myrne taken on July 7, 2020."
|
||||
afterSrc={afterImg}
|
||||
afterAlt="Satellite image of Russian base at Myrne taken on Oct. 20, 2020."
|
||||
>
|
||||
{#snippet beforeOverlay()}
|
||||
<div class="overlay p-3 before">
|
||||
<p class="h4 font-bold">July 7, 2020</p>
|
||||
<p class="body-note">Initially, this site was far smaller.</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet afterOverlay()}
|
||||
<div class="overlay p-3 after">
|
||||
<p class="h4 font-bold">Oct. 20, 2020</p>
|
||||
<p class="body-note">But then forces built up.</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet caption()}
|
||||
<p class="body-note">Photos by MAXAR Technologies, 2021.</p>
|
||||
{/snippet}
|
||||
</BeforeAfter>
|
||||
|
||||
<style lang="scss">
|
||||
.overlay {
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
max-width: 350px;
|
||||
p {
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</Story>
|
||||
376
src/components/BeforeAfter/BeforeAfter.svelte
Normal file
|
|
@ -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>
|
||||
BIN
src/components/BeforeAfter/images/myrne-after.jpg
Normal file
|
After Width: | Height: | Size: 715 KiB |
BIN
src/components/BeforeAfter/images/myrne-before.jpg
Normal file
|
After Width: | Height: | Size: 472 KiB |
|
|
@ -42,7 +42,7 @@
|
|||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '../../scss/mixins' as mixins;
|
||||
@use 'mixins' as mixins;
|
||||
|
||||
.article-block {
|
||||
max-width: var(--normal-column-width, 660px);
|
||||
|
|
|
|||
170
src/components/Byline/Byline.mdx
Normal 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 {/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',
|
||||
})} {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} />
|
||||
````
|
||||
108
src/components/Byline/Byline.stories.svelte
Normal 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 {/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',
|
||||
})} {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`;
|
||||
},
|
||||
}}
|
||||
/>
|
||||
181
src/components/Byline/Byline.svelte
Normal 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 {/if}
|
||||
{/each}
|
||||
{:else}
|
||||
<a
|
||||
href="https://www.reuters.com"
|
||||
class="no-underline whitespace-nowrap text-primary font-bold"
|
||||
>Reuters</a
|
||||
>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="dateline body-caption fmt-0">
|
||||
{#if published}
|
||||
<div class="whitespace-nowrap inline-block">
|
||||
<!-- Custom published dateline snippet -->
|
||||
<time datetime={publishTime}>
|
||||
{@render published()}
|
||||
</time>
|
||||
</div>
|
||||
{:else if isValidDate(publishTime)}
|
||||
<div class="whitespace-nowrap inline-block">
|
||||
Published
|
||||
<time datetime={publishTime}>
|
||||
{#if updateTime && isValidDate(updateTime)}
|
||||
{apdate(new Date(publishTime))}
|
||||
{:else}
|
||||
{apdate(new Date(publishTime))} {formatTime(
|
||||
publishTime
|
||||
)}
|
||||
{/if}
|
||||
</time>
|
||||
</div>
|
||||
{/if}
|
||||
{#if updated}
|
||||
<div class="whitespace-nowrap inline-block">
|
||||
<!-- Custom updated dateline snippet -->
|
||||
<time datetime={updateTime}>
|
||||
{@render updated()}
|
||||
</time>
|
||||
</div>
|
||||
{:else if isValidDate(publishTime) && isValidDate(updateTime || '')}
|
||||
<div class="whitespace-nowrap inline-block">
|
||||
Last updated
|
||||
<time datetime={updateTime}>
|
||||
{#if areSameDay(new Date(publishTime), new Date(updateTime || new Date()))}
|
||||
{formatTime(updateTime || '')}
|
||||
{:else}
|
||||
{apdate(
|
||||
new Date(updateTime || new Date())
|
||||
)} {formatTime(updateTime || '')}
|
||||
{/if}
|
||||
</time>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</aside>
|
||||
</Block>
|
||||
|
||||
<style lang="scss">
|
||||
@use '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>
|
||||
264
src/components/ClockWall/Clock.svelte
Normal 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>
|
||||
27
src/components/ClockWall/ClockWall.mdx
Normal 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} />
|
||||
21
src/components/ClockWall/ClockWall.stories.svelte
Normal 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" />
|
||||
61
src/components/ClockWall/ClockWall.svelte
Normal 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>
|
||||
45
src/components/DatawrapperChart/DatawrapperChart.mdx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { Meta, Canvas } from '@storybook/blocks';
|
||||
|
||||
import * as DatawrapperChartStories from './DatawrapperChart.stories.svelte';
|
||||
|
||||
<Meta of={DatawrapperChartStories} />
|
||||
|
||||
# DatawrapperChart
|
||||
|
||||
Easily add a responsive Datawrapper embed on your page.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { DatawrapperChart } from '@reuters-graphics/graphics-components';
|
||||
</script>
|
||||
|
||||
<DatawrapperChart
|
||||
title="Global abortion access"
|
||||
ariaLabel="map"
|
||||
id="abortion-rights-map"
|
||||
src="https://graphics.reuters.com/USA-ABORTION/lgpdwggnwvo/media-embed.html"
|
||||
/>
|
||||
```
|
||||
|
||||
##### Getting the chart URL for `src`
|
||||
|
||||
Copy the source url for the Datawrapper chart in the `src` prop.
|
||||
You can get this from the published url on Reuters Graphics.
|
||||
|
||||
- Publish the chart on Datawrapper.
|
||||
- Go to the **Datawrapper charts** Teams channel, wait for the graphic to finish publishing.
|
||||
- Inside **Embed code (for developers only)**, find and copy the url inside the `src` prop. (It ends in `media-embed.html`.)
|
||||
|
||||
**Note:** There is no need to update the url if you update the chart inside Datawrapper. Any changes will be automatically reflected.
|
||||
|
||||
<Canvas of={DatawrapperChartStories.Demo} />
|
||||
|
||||
## With chatter
|
||||
|
||||
By default, Datawrapper will export your chart with the chart chatter like title, description and notes.
|
||||
|
||||
At the moment, these don't _exactly_ match our styles and can't be made to fit into the article well.
|
||||
|
||||
Instead, it's often better to remove all the text from your Datawrapper chart before publishing it and add that text back via the component props.
|
||||
|
||||
<Canvas of={DatawrapperChartStories.WithChatter} />
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import DatawrapperChart from './DatawrapperChart.svelte';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Components/Graphics/DatawrapperChart',
|
||||
component: DatawrapperChart,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
width: {
|
||||
control: 'select',
|
||||
options: ['normal', 'wide', 'wider', 'widest', 'fluid'],
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story
|
||||
name="Demo"
|
||||
args={{
|
||||
src: 'https://reuters.com/graphics/USA-ABORTION/lgpdwggnwvo/media-embed.html',
|
||||
id: 'abortion-rights-map',
|
||||
ariaLabel: 'map',
|
||||
frameTitle: 'Global abortion access',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Story
|
||||
name="With chatter"
|
||||
tags={['!autodocs']}
|
||||
args={{
|
||||
frameTitle: 'Global abortion access',
|
||||
ariaLabel: 'map',
|
||||
id: 'abortion-rights-map',
|
||||
src: 'https://reuters.com/graphics/USA-ABORTION/lgvdwemlbpo/media-embed.html',
|
||||
title: 'Global abortion access',
|
||||
description: 'A map of worldwide access to abortion.',
|
||||
notes:
|
||||
'Note: Different indicators and additional restrictions, including different gestational limits, apply in some countries. Refer to source for full classification. Current as of May 4, 2022.\n\nSource: Center for Reproductive Rights',
|
||||
}}
|
||||
/>
|
||||
117
src/components/DatawrapperChart/DatawrapperChart.svelte
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
<!-- @component `DatawrapperChart` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-graphics-datawrapperchart--docs) -->
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy, type Snippet } from 'svelte';
|
||||
import GraphicBlock from '../GraphicBlock/GraphicBlock.svelte';
|
||||
import type { ContainerWidth } from '../@types/global';
|
||||
|
||||
type ScrollingOption = 'auto' | 'yes' | 'no';
|
||||
|
||||
interface Props {
|
||||
/** Title of the graphic */
|
||||
title?: string;
|
||||
/** Description of the graphic, passed in as a markdown string. */
|
||||
description?: string;
|
||||
/**
|
||||
* iframe title
|
||||
*/
|
||||
frameTitle: string;
|
||||
/**
|
||||
* Notes to the graphic, passed in as a markdown string.
|
||||
*/
|
||||
notes?: string;
|
||||
/**
|
||||
* iframe aria label
|
||||
*/
|
||||
ariaLabel: string;
|
||||
/*
|
||||
* iframe id
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Datawrapper embed URL
|
||||
*/
|
||||
src: string;
|
||||
/** iframe scrolling option */
|
||||
scrolling: ScrollingOption;
|
||||
/** Width of the chart within the text well. */
|
||||
width: ContainerWidth;
|
||||
/**
|
||||
* Set a different width for the text within the text well, for example,
|
||||
* "normal" to keep the title, description and notes inline with the rest
|
||||
* of the text well. Can't ever be wider than `width`.
|
||||
*/
|
||||
textWidth: ContainerWidth;
|
||||
/** Custom headline and chatter snippet */
|
||||
titleSnippet?: Snippet;
|
||||
/** Custom notes and source snippet */
|
||||
notesSnippet?: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
title,
|
||||
description,
|
||||
frameTitle = '',
|
||||
notes,
|
||||
ariaLabel = '',
|
||||
id = '',
|
||||
src,
|
||||
scrolling = 'no',
|
||||
width = 'normal',
|
||||
textWidth = 'normal',
|
||||
titleSnippet,
|
||||
notesSnippet,
|
||||
}: Props = $props();
|
||||
|
||||
let frameElement: HTMLElement;
|
||||
|
||||
// eslint-disable-next-line
|
||||
const frameFiller = (e: any) => {
|
||||
if (void 0 !== e.data['datawrapper-height']) {
|
||||
const t = [frameElement];
|
||||
for (const a in e.data['datawrapper-height']) {
|
||||
for (let r = 0; r < t.length; r++) {
|
||||
// @ts-ignore OK here
|
||||
if (t[r].contentWindow === e.source) {
|
||||
t[r].style.height = e.data['datawrapper-height'][a] + 'px';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('message', frameFiller);
|
||||
}
|
||||
});
|
||||
onDestroy(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('message', frameFiller);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<GraphicBlock {width} {textWidth} {title} {description} {notes}>
|
||||
{#if titleSnippet}
|
||||
<!-- Custom headline and chatter slot -->
|
||||
{@render titleSnippet()}
|
||||
{/if}
|
||||
|
||||
<div class="datawrapper-chart">
|
||||
<iframe
|
||||
bind:this={frameElement}
|
||||
title={frameTitle}
|
||||
aria-label={ariaLabel}
|
||||
{id}
|
||||
{src}
|
||||
{scrolling}
|
||||
frameborder="0"
|
||||
data-chromatic="ignore"
|
||||
style="width: 0; min-width: 100% !important; border: none;"
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
{#if notesSnippet}
|
||||
{@render notesSnippet()}
|
||||
{/if}
|
||||
</GraphicBlock>
|
||||
26
src/components/DocumentCloud/DocumentCloud.mdx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { Meta, Canvas } from '@storybook/blocks';
|
||||
|
||||
import * as DocumentCloudStories from './DocumentCloud.stories.svelte';
|
||||
|
||||
<Meta of={DocumentCloudStories} />
|
||||
|
||||
# DocumentCloud
|
||||
|
||||
The `DocumentCloud` component embeds a document hosted by [DocumentCloud](https://documentcloud.org).
|
||||
|
||||
The document must have its access level set to **public** before it can be embedded. The `slug` can be found after the final slash in the document's URL.
|
||||
|
||||
For instance, the document included in the example is found at [documentcloud.org/documents/3259984-Trump-Intelligence-Allegations](https://www.documentcloud.org/documents/3259984-Trump-Intelligence-Allegations). The `slug` is `3259984-Trump-Intelligence-Allegations`.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { DocumentCloud } from '@reuters-graphics/graphics-components';
|
||||
</script>
|
||||
|
||||
<DocumentCloud
|
||||
slug="3259984-Trump-Intelligence-Allegations"
|
||||
altText="These Reports Allege Trump Has Deep Ties To Russia"
|
||||
/>
|
||||
```
|
||||
|
||||
<Canvas of={DocumentCloudStories.Demo} />
|
||||
23
src/components/DocumentCloud/DocumentCloud.stories.svelte
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import DocumentCloud from './DocumentCloud.svelte';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Components/Multimedia/DocumentCloud',
|
||||
component: DocumentCloud,
|
||||
argTypes: {
|
||||
width: {
|
||||
control: 'select',
|
||||
options: ['normal', 'wide', 'wider', 'widest', 'fluid'],
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story
|
||||
name="Demo"
|
||||
args={{
|
||||
slug: '3259984-Trump-Intelligence-Allegations',
|
||||
altText: 'These Reports Allege Trump Has Deep Ties To Russia',
|
||||
}}
|
||||
/>
|
||||
41
src/components/DocumentCloud/DocumentCloud.svelte
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<!-- @component `DocumentCloud` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-multimedia-documentcloud--docs) -->
|
||||
<script lang="ts">
|
||||
import type { ContainerWidth } from '../@types/global';
|
||||
import Block from '../Block/Block.svelte';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* The unique identifier for the document.
|
||||
*/
|
||||
slug: string;
|
||||
/**
|
||||
* Alt text for the document.
|
||||
*/
|
||||
altText: string;
|
||||
/**
|
||||
* Width of the container, one of: normal, wide, wider, widest or fluid
|
||||
*/
|
||||
width?: ContainerWidth;
|
||||
/** Add an ID to target with SCSS. */
|
||||
id?: string;
|
||||
/** Add a class to target with SCSS. */
|
||||
class?: string; // Add a class to target with SCSS.
|
||||
}
|
||||
|
||||
let {
|
||||
slug,
|
||||
altText,
|
||||
width = 'normal',
|
||||
id = '',
|
||||
class: cls = '',
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<Block {width} {id} class="photo fmy-6 {cls}">
|
||||
<iframe
|
||||
class="h-screen"
|
||||
src="https://embed.documentcloud.org/documents/{slug}/?embed=1&responsive=1&title=1"
|
||||
title={altText}
|
||||
sandbox="allow-scripts allow-same-origin allow-popups allow-forms allow-popups-to-escape-sandbox"
|
||||
></iframe>
|
||||
</Block>
|
||||
19
src/components/EmbedPreviewerLink/EmbedPreviewerLink.mdx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { Meta } from '@storybook/blocks';
|
||||
|
||||
import * as EmbedPreviewerLinkStories from './EmbedPreviewerLink.stories.svelte';
|
||||
|
||||
<Meta of={EmbedPreviewerLinkStories} />
|
||||
|
||||
# EmbedPreviewerLink
|
||||
|
||||
The `EmbedPreviewerLink` component is a tool for previewing the embeds in development. It adds an icon at the bottom of the page that, when clicked, opens a previewer with the embeds.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { EmbedPreviewerLink } from '@reuters-graphics/graphics-components';
|
||||
|
||||
import { dev } from '$app/env';
|
||||
</script>
|
||||
|
||||
<EmbedPreviewerLink {dev} />
|
||||
```
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<script module lang="ts">
|
||||
import EmbedPreviewerLink from './EmbedPreviewerLink.svelte';
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Components/Utilities/EmbedPreviewerLink',
|
||||
component: EmbedPreviewerLink,
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story
|
||||
name="Demo"
|
||||
tags={['!autodocs', '!dev']}
|
||||
args={{
|
||||
dev: true,
|
||||
}}
|
||||
/>
|
||||
32
src/components/EmbedPreviewerLink/EmbedPreviewerLink.svelte
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<script lang="ts">
|
||||
import Fa from 'svelte-fa';
|
||||
import { faWindowRestore } from '@fortawesome/free-regular-svg-icons';
|
||||
interface Props {
|
||||
dev?: boolean;
|
||||
}
|
||||
|
||||
let { dev = false }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if dev}
|
||||
<div>
|
||||
<a rel="external" href="/embed-previewer">
|
||||
<Fa icon={faWindowRestore} />
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
div {
|
||||
position: fixed;
|
||||
bottom: 5px;
|
||||
left: 10px;
|
||||
font-size: 18px;
|
||||
a {
|
||||
color: #ccc;
|
||||
&:hover {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
67
src/components/EndNotes/EndNotes.mdx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { Meta, Canvas } from '@storybook/blocks';
|
||||
|
||||
import * as EndNotesStories from './EndNotes.stories.svelte';
|
||||
|
||||
<Meta of={EndNotesStories} />
|
||||
|
||||
# EndNotes
|
||||
|
||||
The `EndNotes` component adds notes such as sources, clarifiying notes and minor corrections that come at the end of a story.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { EndNotes } from '@reuters-graphics/graphics-components';
|
||||
|
||||
const notes = [
|
||||
{
|
||||
title: 'Note',
|
||||
text: 'Data is current as of today.',
|
||||
},
|
||||
{
|
||||
title: 'Sources',
|
||||
text: 'Data, Inc.',
|
||||
},
|
||||
{
|
||||
title: 'Edited by',
|
||||
text: '<a href="https://www.reuters.com/graphics/">Editor</a>, Copyeditor',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<EndNotes {notes} />
|
||||
```
|
||||
|
||||
<Canvas of={EndNotesStories.Demo} />
|
||||
|
||||
## Using with ArchieML docs
|
||||
|
||||
With the graphics kit, you'll likely get your text value from an ArchieML doc...
|
||||
|
||||
```yaml
|
||||
# ArchieML doc
|
||||
[endNotes]
|
||||
title: Note
|
||||
text: Data is current as of today
|
||||
|
||||
title: Sources
|
||||
text: Data, Inc.
|
||||
|
||||
title: Edited by
|
||||
text: Editor, Copyeditor
|
||||
[]
|
||||
```
|
||||
|
||||
... which you'll pass to the `EndNotes` component.
|
||||
|
||||
```svelte
|
||||
<!-- graphics kit -->
|
||||
<script>
|
||||
import { EndNotes } from '@reuters-graphics/graphics-components';
|
||||
|
||||
import content from '$locales/en/content.json';
|
||||
</script>
|
||||
|
||||
<EndNotes notes={content.endNotes} />
|
||||
```
|
||||
|
||||
<Canvas of={EndNotesStories.Demo} />
|
||||
29
src/components/EndNotes/EndNotes.stories.svelte
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<script module lang="ts">
|
||||
import EndNotes from './EndNotes.svelte';
|
||||
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Components/Text elements/EndNotes',
|
||||
component: EndNotes,
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
const notes = [
|
||||
{
|
||||
title: 'Note',
|
||||
text: 'Data is current as of today.',
|
||||
},
|
||||
{
|
||||
title: 'Sources',
|
||||
text: 'Data, Inc.',
|
||||
},
|
||||
{
|
||||
title: 'Edited by',
|
||||
text: '<a href="https://www.reuters.com/graphics/">Editor</a>, Copyeditor',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<Story name="Demo" args={{ notes }} />
|
||||
58
src/components/EndNotes/EndNotes.svelte
Normal file
|
|
@ -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>
|
||||
72
src/components/FeaturePhoto/FeaturePhoto.mdx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { Meta, Canvas } from '@storybook/blocks';
|
||||
|
||||
import * as FeaturePhotoStories from './FeaturePhoto.stories.svelte';
|
||||
|
||||
<Meta of={FeaturePhotoStories} />
|
||||
|
||||
# FeaturePhoto
|
||||
|
||||
The `FeaturePhoto` component adds a full-width photo.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { FeaturePhoto } from '@reuters-graphics/graphics-components';
|
||||
import { assets } from '$app/paths'; // 👈 If using in the graphics kit...
|
||||
</script>
|
||||
|
||||
<FeaturePhoto
|
||||
src={`${assets}/images/myImage.jpg`}
|
||||
altText="Some alt text"
|
||||
caption="A caption"
|
||||
/>
|
||||
```
|
||||
|
||||
<Canvas of={FeaturePhotoStories.Demo} />
|
||||
|
||||
## Using with ArchieML docs
|
||||
|
||||
With the graphics kit, you'll likely get your text value from an ArchieML doc...
|
||||
|
||||
```yaml
|
||||
# ArchieML doc
|
||||
[blocks]
|
||||
|
||||
type: photo
|
||||
width: normal
|
||||
src: images/shark.jpg
|
||||
altText: The king of the sea
|
||||
caption: Carcharodon carcharias - REUTERS
|
||||
|
||||
[]
|
||||
```
|
||||
|
||||
... which you'll parse out of a ArchieML block object before passing to the `FeaturePhoto` component.
|
||||
|
||||
```svelte
|
||||
<!-- App.svelte -->
|
||||
<script>
|
||||
import { FeaturePhoto } from '@reuters-graphics/graphics-components';
|
||||
|
||||
import content from '$locales/en/content.json';
|
||||
import { assets } from '$app/paths';
|
||||
</script>
|
||||
|
||||
{#each content.blocks as block}
|
||||
{#if block.Type === 'text'}
|
||||
<!-- ... -->
|
||||
{:else if block.type === 'photo'}
|
||||
<FeaturePhoto
|
||||
width={block.width}
|
||||
src={`${assets}/${block.src}`}
|
||||
altText={block.altText}
|
||||
caption={block.caption}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
```
|
||||
|
||||
## Missing alt text
|
||||
|
||||
`altText` is required in this component. If your photo is missing it, a small red text box will overlay the image.
|
||||
|
||||
<Canvas of={FeaturePhotoStories.MissingAltText} />
|
||||
41
src/components/FeaturePhoto/FeaturePhoto.stories.svelte
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import FeaturePhoto from './FeaturePhoto.svelte';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Components/Multimedia/FeaturePhoto',
|
||||
component: FeaturePhoto,
|
||||
argTypes: {
|
||||
width: {
|
||||
control: 'select',
|
||||
options: ['normal', 'wide', 'wider', 'widest', 'fluid'],
|
||||
},
|
||||
textWidth: {
|
||||
control: 'select',
|
||||
options: ['normal', 'wide', 'wider', 'widest', 'fluid'],
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import sharkSrc from './images/shark.jpg';
|
||||
</script>
|
||||
|
||||
<Story
|
||||
name="Demo"
|
||||
args={{
|
||||
src: sharkSrc,
|
||||
altText: 'A shark!',
|
||||
caption: 'Carcharodon carcharias - REUTERS',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Story
|
||||
name="Missing altText"
|
||||
exportName="MissingAltText"
|
||||
args={{
|
||||
src: sharkSrc,
|
||||
caption: 'Carcharodon carcharias - REUTERS',
|
||||
}}
|
||||
/>
|
||||
145
src/components/FeaturePhoto/FeaturePhoto.svelte
Normal file
|
|
@ -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>
|
||||
BIN
src/components/FeaturePhoto/images/shark.jpg
Normal file
|
After Width: | Height: | Size: 103 KiB |
|
|
@ -21,7 +21,7 @@
|
|||
</form>
|
||||
|
||||
<style lang="scss">
|
||||
@use '../../../scss/mixins' as mixins;
|
||||
@use 'mixins' as mixins;
|
||||
|
||||
label {
|
||||
margin-bottom: 0.25rem;
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@
|
|||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
@use '../../scss/mixins' as mixins;
|
||||
@use 'mixins' as mixins;
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
|
|
|
|||
|
|
@ -283,7 +283,7 @@
|
|||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '../../../scss/mixins' as mixins;
|
||||
@use 'mixins' as mixins;
|
||||
|
||||
[data-svelte-typeahead] {
|
||||
position: relative;
|
||||
|
|
|
|||
23
src/components/Functions/Utils.mdx
Normal 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.'
|
||||
```
|
||||
9
src/components/Functions/Utils.stories.svelte
Normal 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']} />
|
||||
|
|
@ -192,7 +192,7 @@
|
|||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '../../scss/mixins' as mixins;
|
||||
@use 'mixins' as mixins;
|
||||
|
||||
.geocoder-input-wrapper {
|
||||
position: relative;
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@
|
|||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '../../scss/mixins' as mixins;
|
||||
@use 'mixins' as mixins;
|
||||
|
||||
div.container {
|
||||
display: contents;
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@
|
|||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '../../scss/mixins' as mixins;
|
||||
@use 'mixins' as mixins;
|
||||
.headline-wrapper {
|
||||
:global(.dek) {
|
||||
max-width: mixins.$column-width-normal;
|
||||
|
|
|
|||
37
src/components/Headpile/Headpile.mdx
Normal 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} />
|
||||
37
src/components/Headpile/Headpile.stories.svelte
Normal 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 Sudan’s richest men, exporting gold from mines in Darfur seized by his fighters.",
|
||||
colour: '#afb776',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<Story
|
||||
name="Demo"
|
||||
args={{
|
||||
figures: defaultArgs,
|
||||
}}
|
||||
/>
|
||||
81
src/components/Headpile/Headpile.svelte
Normal 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>
|
||||
41
src/components/Headpile/Headshot.svelte
Normal 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>
|
||||
108
src/components/Headpile/KeyFigure.svelte
Normal 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>
|
||||
BIN
src/components/Headpile/images/abdel.png
Normal file
|
After Width: | Height: | Size: 304 KiB |
BIN
src/components/Headpile/images/hemedti.png
Normal file
|
After Width: | Height: | Size: 407 KiB |
329
src/components/HeroHeadline/HeroHeadline.mdx
Normal 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 year’s 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} />
|
||||
287
src/components/HeroHeadline/HeroHeadline.stories.svelte
Normal 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
|
||||
year’s 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>
|
||||
276
src/components/HeroHeadline/HeroHeadline.svelte
Normal 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>
|
||||
BIN
src/components/HeroHeadline/demo/eurovis.jpeg
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
src/components/HeroHeadline/demo/graphics/CRASH_1-lg.jpeg
Normal file
|
After Width: | Height: | Size: 497 KiB |
BIN
src/components/HeroHeadline/demo/graphics/CRASH_1-md.jpeg
Normal file
|
After Width: | Height: | Size: 303 KiB |
BIN
src/components/HeroHeadline/demo/graphics/CRASH_1-sm.jpeg
Normal file
|
After Width: | Height: | Size: 216 KiB |
BIN
src/components/HeroHeadline/demo/graphics/CRASH_1-xl.jpeg
Normal file
|
After Width: | Height: | Size: 628 KiB |
BIN
src/components/HeroHeadline/demo/graphics/CRASH_1-xl_copy.jpeg
Normal file
|
After Width: | Height: | Size: 787 KiB |
BIN
src/components/HeroHeadline/demo/graphics/CRASH_1-xs.jpeg
Normal file
|
After Width: | Height: | Size: 118 KiB |
650
src/components/HeroHeadline/demo/graphics/crash.svelte
Normal 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>
|
||||
BIN
src/components/HeroHeadline/demo/graphics/quake-map-top-lg.jpeg
Normal file
|
After Width: | Height: | Size: 428 KiB |
BIN
src/components/HeroHeadline/demo/graphics/quake-map-top-md.jpeg
Normal file
|
After Width: | Height: | Size: 256 KiB |
BIN
src/components/HeroHeadline/demo/graphics/quake-map-top-sm.jpeg
Normal file
|
After Width: | Height: | Size: 200 KiB |
BIN
src/components/HeroHeadline/demo/graphics/quake-map-top-xl.jpeg
Normal file
|
After Width: | Height: | Size: 486 KiB |
BIN
src/components/HeroHeadline/demo/graphics/quake-map-top-xs.jpeg
Normal file
|
After Width: | Height: | Size: 127 KiB |
863
src/components/HeroHeadline/demo/graphics/quakemap.svelte
Normal 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>
|
||||
BIN
src/components/HeroHeadline/demo/polar.jpg
Normal file
|
After Width: | Height: | Size: 58 KiB |
122
src/components/InfoBox/InfoBox.mdx
Normal 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} />
|
||||
98
src/components/InfoBox/InfoBox.stories.svelte
Normal 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>
|
||||