@reuters-graphics/graphics-components 3.4.1 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,134 @@
1
+ <!-- @component `Bio` renders a single author's avatar, name, title, social links and biography. Used by `BioBox`, but works on its own. -->
2
+ <script lang="ts">
3
+ import KinesisLogo from '../KinesisLogo/KinesisLogo.svelte';
4
+ import SocialLinks from './SocialLinks.svelte';
5
+ import type { Author } from './types';
6
+
7
+ interface Props extends Author {
8
+ /** Extra class on the root element. */
9
+ class?: string;
10
+ }
11
+
12
+ let {
13
+ name,
14
+ title,
15
+ bio,
16
+ imageUrl,
17
+ imageAlt,
18
+ links = [],
19
+ class: cls = '',
20
+ }: Props = $props();
21
+ </script>
22
+
23
+ <div class="bio {cls}">
24
+ <div class="bio-header">
25
+ <div class="bio-identity">
26
+ {#if imageUrl}
27
+ <img class="bio-avatar" src={imageUrl} alt={imageAlt ?? name} />
28
+ {:else}
29
+ <div class="bio-avatar bio-avatar-fallback" aria-hidden="true">
30
+ <KinesisLogo width="60%" />
31
+ </div>
32
+ {/if}
33
+ <div class="bio-names">
34
+ <p class="bio-name">{name}</p>
35
+ {#if title}<p class="bio-title">{title}</p>{/if}
36
+ </div>
37
+ </div>
38
+ {#if links.length}
39
+ <div class="bio-links bio-links-inline">
40
+ <SocialLinks {links} {name} />
41
+ </div>
42
+ {/if}
43
+ </div>
44
+
45
+ {#if bio}
46
+ <p class="bio-text">{bio}</p>
47
+ {/if}
48
+
49
+ {#if links.length}
50
+ <div class="bio-links bio-links-stacked">
51
+ <SocialLinks {links} {name} />
52
+ </div>
53
+ {/if}
54
+ </div>
55
+
56
+ <style>/* Generated from
57
+ https://utopia.fyi/space/calculator/?c=320,18,1.125,1280,21,1.25,7,3,&s=0.75|0.5|0.25,1.5|2|3|4|6,s-l&g=s,l,xl,12
58
+ */
59
+ /* Generated from
60
+ https://utopia.fyi/space/calculator/?c=320,18,1.125,1280,21,1.25,7,3,&s=0.75|0.5|0.25,1.5|2|3|4|6,s-l&g=s,l,xl,12
61
+ */
62
+ /* Scales by 1.125 */
63
+ .bio {
64
+ padding-block: 1rem;
65
+ }
66
+
67
+ .bio-header {
68
+ display: flex;
69
+ align-items: flex-start;
70
+ justify-content: space-between;
71
+ gap: 1rem;
72
+ }
73
+
74
+ .bio-identity {
75
+ display: flex;
76
+ align-items: center;
77
+ gap: 0.75rem;
78
+ }
79
+
80
+ .bio-avatar {
81
+ display: block;
82
+ width: 3rem;
83
+ height: 3rem;
84
+ margin: 0;
85
+ border-radius: 50%;
86
+ object-fit: cover;
87
+ flex-shrink: 0;
88
+ }
89
+
90
+ .bio-avatar-fallback {
91
+ display: flex;
92
+ align-items: center;
93
+ justify-content: center;
94
+ background: var(--theme-colour-background, #fff);
95
+ border: 1px solid var(--theme-colour-brand-rules, #e5e5e5);
96
+ }
97
+
98
+ .bio-name {
99
+ font-family: "Knowledge", "Source Sans Pro", Arial, Helvetica, sans-serif;
100
+ font-weight: 700;
101
+ font-size: var(--theme-font-size-base);
102
+ margin: 0;
103
+ line-height: 1.2;
104
+ color: var(--theme-colour-text-primary, #121212);
105
+ }
106
+
107
+ .bio-title {
108
+ font-family: "Knowledge", "Source Sans Pro", Arial, Helvetica, sans-serif;
109
+ font-size: var(--theme-font-size-sm);
110
+ margin: 0.1rem 0 0;
111
+ color: var(--theme-colour-text-secondary, #666);
112
+ }
113
+
114
+ .bio-text {
115
+ font-family: "Knowledge", "Source Sans Pro", Arial, Helvetica, sans-serif;
116
+ font-size: var(--theme-font-size-sm);
117
+ margin: 0.75rem 0 0;
118
+ line-height: 1.5;
119
+ color: var(--theme-colour-text-primary, #121212);
120
+ }
121
+
122
+ .bio-links-stacked {
123
+ display: none;
124
+ }
125
+
126
+ @media (max-width: 520px) {
127
+ .bio-links-inline {
128
+ display: none;
129
+ }
130
+ .bio-links-stacked {
131
+ display: block;
132
+ margin-top: 0.75rem;
133
+ }
134
+ }</style>
@@ -0,0 +1,9 @@
1
+ import type { Author } from './types';
2
+ interface Props extends Author {
3
+ /** Extra class on the root element. */
4
+ class?: string;
5
+ }
6
+ /** `Bio` renders a single author's avatar, name, title, social links and biography. Used by `BioBox`, but works on its own. */
7
+ declare const Bio: import("svelte").Component<Props, {}, "">;
8
+ type Bio = ReturnType<typeof Bio>;
9
+ export default Bio;
@@ -0,0 +1,50 @@
1
+ <!-- @component `BioBox` shows one or more author biographies in a bordered box, echoing the contributor box on Reuters.com stories. [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-page-furniture-biobox--docs) -->
2
+ <script lang="ts">
3
+ import Block from '../Block/Block.svelte';
4
+ import Bio from './Bio.svelte';
5
+ import type { Author } from './types';
6
+
7
+ interface Props {
8
+ /** Authors to display, in order. */
9
+ authors: Author[];
10
+ /** ID on the containing block. */
11
+ id?: string;
12
+ /** Extra classes on the containing block. */
13
+ class?: string;
14
+ }
15
+
16
+ let { authors, id = '', class: cls = 'fmy-8' }: Props = $props();
17
+ </script>
18
+
19
+ {#if authors.length}
20
+ <Block {id} class="biobox {cls}">
21
+ <div class="biobox-inner">
22
+ {#each authors as author, i}
23
+ <Bio {...author} />
24
+ {#if i < authors.length - 1}
25
+ <hr class="biobox-divider" />
26
+ {/if}
27
+ {/each}
28
+ </div>
29
+ </Block>
30
+ {/if}
31
+
32
+ <style>.biobox-inner {
33
+ padding: 0.5rem 1.25rem;
34
+ border: 1px solid var(--theme-colour-brand-rules, #d3d3d3);
35
+ border-radius: 4px;
36
+ background: var(--theme-colour-background, #fff);
37
+ }
38
+
39
+ .biobox-divider {
40
+ border: 0;
41
+ border-top: 1px solid var(--theme-colour-brand-rules, #d3d3d3);
42
+ opacity: 0.5;
43
+ margin: 0.5rem 0;
44
+ }
45
+
46
+ @media (max-width: 767px) {
47
+ .biobox-inner {
48
+ padding-inline: 1rem;
49
+ }
50
+ }</style>
@@ -0,0 +1,13 @@
1
+ import type { Author } from './types';
2
+ interface Props {
3
+ /** Authors to display, in order. */
4
+ authors: Author[];
5
+ /** ID on the containing block. */
6
+ id?: string;
7
+ /** Extra classes on the containing block. */
8
+ class?: string;
9
+ }
10
+ /** `BioBox` shows one or more author biographies in a bordered box, echoing the contributor box on Reuters.com stories. [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-page-furniture-biobox--docs) */
11
+ declare const BioBox: import("svelte").Component<Props, {}, "">;
12
+ type BioBox = ReturnType<typeof BioBox>;
13
+ export default BioBox;
@@ -0,0 +1,168 @@
1
+ <!-- @component `SocialLinks` renders a row of accessible icon links (email, X, LinkedIn, etc.) for a single author. Used by `Bio`. -->
2
+ <script lang="ts">
3
+ import type { SocialLink, SocialPlatform } from './types';
4
+
5
+ interface Props {
6
+ /** Links to render. Unknown platforms fall back to a generic link icon. */
7
+ links: SocialLink[];
8
+ /** Author name, used to build default accessible labels. */
9
+ name?: string;
10
+ }
11
+
12
+ let { links, name = '' }: Props = $props();
13
+
14
+ // Normalize an email destination into a usable href.
15
+ function hrefFor(link: SocialLink): string {
16
+ if (link.platform === 'email' && !/^mailto:/i.test(link.url)) {
17
+ return `mailto:${link.url}`;
18
+ }
19
+ return link.url;
20
+ }
21
+
22
+ // Default, screen-reader-friendly label per platform.
23
+ const PLATFORM_NOUN: Record<SocialPlatform, string> = {
24
+ email: 'Email',
25
+ x: 'X profile of',
26
+ twitter: 'X profile of',
27
+ linkedin: 'LinkedIn profile of',
28
+ facebook: 'Facebook profile of',
29
+ instagram: 'Instagram profile of',
30
+ bluesky: 'Bluesky profile of',
31
+ link: 'Profile of',
32
+ };
33
+
34
+ function labelFor(link: SocialLink): string {
35
+ if (link.label) return link.label;
36
+ const noun = PLATFORM_NOUN[link.platform] ?? PLATFORM_NOUN.link;
37
+ return name ? `${noun} ${name}` : noun;
38
+ }
39
+ </script>
40
+
41
+ <div class="social-links">
42
+ {#each links as link}
43
+ <a
44
+ class="social-link"
45
+ href={hrefFor(link)}
46
+ aria-label={labelFor(link)}
47
+ target={link.platform === 'email' ? null : '_blank'}
48
+ rel={link.platform === 'email' ? null : 'noreferrer'}
49
+ >
50
+ {#if link.platform === 'email'}
51
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
52
+ <rect
53
+ x="3"
54
+ y="5"
55
+ width="18"
56
+ height="14"
57
+ rx="2"
58
+ fill="none"
59
+ stroke="currentColor"
60
+ stroke-width="1.6"
61
+ />
62
+ <path
63
+ d="M4 7l8 6 8-6"
64
+ fill="none"
65
+ stroke="currentColor"
66
+ stroke-width="1.6"
67
+ stroke-linecap="round"
68
+ stroke-linejoin="round"
69
+ />
70
+ </svg>
71
+ {:else if link.platform === 'x' || link.platform === 'twitter'}
72
+ <svg viewBox="0 0 30 30" aria-hidden="true" focusable="false">
73
+ <path
74
+ fill="currentColor"
75
+ d="M17.923 14.387 25.569 5.5h-1.812l-6.64 7.717L11.817 5.5H5.7l8.018 11.67L5.7 26.49h1.812l7.01-8.15 5.6 8.15h6.116l-8.316-12.102Zm-2.482 2.885-.812-1.162-6.464-9.246h2.783l5.216 7.462.813 1.162 6.78 9.7h-2.782l-5.534-7.915Z"
76
+ />
77
+ </svg>
78
+ {:else if link.platform === 'linkedin'}
79
+ <svg viewBox="0 0 21 21" aria-hidden="true" focusable="false">
80
+ <path
81
+ fill="currentColor"
82
+ d="M19.031 0c1.034 0 1.888.807 1.964 1.822L21 1.97V19.03a1.975 1.975 0 0 1-1.822 1.964L19.03 21H1.97a1.975 1.975 0 0 1-1.964-1.822L0 19.03V1.97C0 .935.807.08 1.822.005L1.97 0H19.03ZM6.3 7.875H3.15v10.063H6.3V7.874Zm7.875-.175c-1.575 0-2.538.788-2.975 1.575v-1.4H8.225v10.063h3.15V12.95c0-1.313.175-2.537 1.838-2.537 1.575 0 1.575 1.487 1.575 2.624v4.9h3.15v-5.425c0-2.712-.613-4.812-3.763-4.812ZM4.637 2.8c-1.05 0-1.837.875-1.837 1.838 0 1.05.875 1.837 1.838 1.837 1.05 0 1.837-.787 1.837-1.837 0-1.05-.875-1.838-1.837-1.838Z"
83
+ />
84
+ </svg>
85
+ {:else if link.platform === 'facebook'}
86
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
87
+ <path
88
+ fill="currentColor"
89
+ d="M13.5 21v-8h2.7l.4-3.1h-3.1V7.9c0-.9.25-1.5 1.55-1.5H17V3.6c-.3 0-1.3-.1-2.45-.1-2.43 0-4.05 1.48-4.05 4.2v2.2H7.8V13h2.7v8h3Z"
90
+ />
91
+ </svg>
92
+ {:else if link.platform === 'instagram'}
93
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
94
+ <rect
95
+ x="3.5"
96
+ y="3.5"
97
+ width="17"
98
+ height="17"
99
+ rx="5"
100
+ fill="none"
101
+ stroke="currentColor"
102
+ stroke-width="1.6"
103
+ />
104
+ <circle
105
+ cx="12"
106
+ cy="12"
107
+ r="4"
108
+ fill="none"
109
+ stroke="currentColor"
110
+ stroke-width="1.6"
111
+ />
112
+ <circle cx="17.2" cy="6.8" r="1.1" fill="currentColor" />
113
+ </svg>
114
+ {:else if link.platform === 'bluesky'}
115
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
116
+ <path
117
+ fill="currentColor"
118
+ d="M12 10.8C10.9 8.6 7.9 4.6 5.2 3.2 2.6 1.9 1.6 2.4 1 2.9.3 3.5 0 4.7 0 5.4c0 .7.4 5.6.6 6.4.7 2.7 3.5 3.6 6.1 3.3-3.8.6-7.2 2-2.8 6.9 4.9 5 6.7-1 7.6-4.1.9 3.1 2 8.9 7.5 4.1 4.1-4.1 1.1-6.3-2.7-6.9 2.6.3 5.4-.6 6.1-3.3.2-.8.6-5.7.6-6.4 0-.7-.3-1.9-1-2.5-.6-.5-1.6-1-4.2.3C16.1 4.6 13.1 8.6 12 10.8Z"
119
+ />
120
+ </svg>
121
+ {:else}
122
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
123
+ <path
124
+ d="M10 14a3.5 3.5 0 0 0 5 0l3-3a3.5 3.5 0 0 0-5-5l-1.5 1.5M14 10a3.5 3.5 0 0 0-5 0l-3 3a3.5 3.5 0 0 0 5 5l1.5-1.5"
125
+ fill="none"
126
+ stroke="currentColor"
127
+ stroke-width="1.6"
128
+ stroke-linecap="round"
129
+ stroke-linejoin="round"
130
+ />
131
+ </svg>
132
+ {/if}
133
+ </a>
134
+ {/each}
135
+ </div>
136
+
137
+ <style>.social-links {
138
+ display: inline-flex;
139
+ gap: 0.5rem;
140
+ flex-wrap: wrap;
141
+ }
142
+
143
+ .social-link {
144
+ display: inline-flex;
145
+ align-items: center;
146
+ justify-content: center;
147
+ width: 2rem;
148
+ height: 2rem;
149
+ border: 1px solid var(--theme-colour-brand-rules, #d3d3d3);
150
+ border-radius: 4px;
151
+ color: var(--theme-colour-text-secondary, #666);
152
+ background: transparent;
153
+ text-decoration: none;
154
+ transition: color 0.15s ease, border-color 0.15s ease, background-color 0.15s ease;
155
+ }
156
+
157
+ .social-link:hover,
158
+ .social-link:focus-visible {
159
+ color: var(--theme-colour-text-primary, #121212);
160
+ border-color: var(--theme-colour-text-secondary, #666);
161
+ background: rgba(0, 0, 0, 0.03);
162
+ }
163
+
164
+ .social-link svg {
165
+ width: 1rem;
166
+ height: 1rem;
167
+ display: block;
168
+ }</style>
@@ -0,0 +1,11 @@
1
+ import type { SocialLink } from './types';
2
+ interface Props {
3
+ /** Links to render. Unknown platforms fall back to a generic link icon. */
4
+ links: SocialLink[];
5
+ /** Author name, used to build default accessible labels. */
6
+ name?: string;
7
+ }
8
+ /** `SocialLinks` renders a row of accessible icon links (email, X, LinkedIn, etc.) for a single author. Used by `Bio`. */
9
+ declare const SocialLinks: import("svelte").Component<Props, {}, "">;
10
+ type SocialLinks = ReturnType<typeof SocialLinks>;
11
+ export default SocialLinks;
@@ -0,0 +1,32 @@
1
+ /** Social/contact platforms a bio can link to. */
2
+ export type SocialPlatform = 'email' | 'x' | 'twitter' | 'linkedin' | 'facebook' | 'instagram' | 'bluesky' | 'link';
3
+ /** A single contact or social link shown beside an author's name. */
4
+ export interface SocialLink {
5
+ /** Which platform this link points to. Controls the icon shown. */
6
+ platform: SocialPlatform;
7
+ /**
8
+ * Destination. For `email`, either a bare address or a `mailto:` URL — the
9
+ * component adds the `mailto:` prefix if it's missing.
10
+ */
11
+ url: string;
12
+ /**
13
+ * Accessible label for the link, read by screen readers. Defaults to a
14
+ * sensible per-platform label that includes the author's name.
15
+ */
16
+ label?: string;
17
+ }
18
+ /** One author/contributor rendered as a row inside `BioBox`. */
19
+ export interface Author {
20
+ /** Full name, shown in bold. */
21
+ name: string;
22
+ /** Job title, shown under the name, e.g. "Climate reporter". */
23
+ title?: string;
24
+ /** Short biography. Plain text. */
25
+ bio?: string;
26
+ /** Avatar image URL. Falls back to the Reuters Kinesis logo when omitted. */
27
+ imageUrl?: string;
28
+ /** Alt text for the avatar image. Defaults to the author's name. */
29
+ imageAlt?: string;
30
+ /** Contact and social links shown beside the name. */
31
+ links?: SocialLink[];
32
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -110,6 +110,11 @@ https://utopia.fyi/space/calculator/?c=320,18,1.125,1280,21,1.25,7,3,&s=0.75|0.5
110
110
  .infobox :global(.article-block) {
111
111
  border-color: var(--theme-colour-brand-rules);
112
112
  }
113
+ @media (max-width: 767px) {
114
+ .infobox :global(.article-block) {
115
+ padding-inline: 1rem;
116
+ }
117
+ }
113
118
  .infobox.light :global(.article-block) {
114
119
  background-color: rgb(250, 250, 250);
115
120
  }