@reuters-graphics/graphics-components 3.2.1 → 3.3.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,228 @@
1
+ <script lang="ts">
2
+ import Block from '../Block/Block.svelte';
3
+ import TOCList from './TOCList.svelte';
4
+ import { apmonth } from 'journalize';
5
+ import { slugify } from '../../utils';
6
+ import { slide } from 'svelte/transition';
7
+ import Fa from 'svelte-fa';
8
+ import {
9
+ faCaretDown,
10
+ faAngleDoubleUp,
11
+ faAngleDoubleDown,
12
+ } from '@fortawesome/free-solid-svg-icons';
13
+
14
+ interface Post {
15
+ title: string;
16
+ slugTitle: string;
17
+ publishTime: string;
18
+ }
19
+
20
+ interface Props {
21
+ posts: Post[];
22
+ /** Base path prepended to post links, e.g. "/graphics". */
23
+ base: string;
24
+ /** The label for the table of contents toggle button. */
25
+ label?: string;
26
+ /** The maximum height of the table of contents list in pixels. */
27
+ maxHeight?: number;
28
+ }
29
+
30
+ let {
31
+ posts,
32
+ base = '',
33
+ label = 'All posts',
34
+ maxHeight = 600,
35
+ }: Props = $props();
36
+
37
+ let showContents = $state(false);
38
+ let scrollPos = $state(0);
39
+ let listHeight = $state(0);
40
+
41
+ const contents = $derived(
42
+ [...posts]
43
+ .sort(
44
+ (a, b) =>
45
+ new Date(a.publishTime).getTime() - new Date(b.publishTime).getTime()
46
+ )
47
+ .map((post) => ({
48
+ date: `${apmonth(new Date(post.publishTime))} ${new Date(post.publishTime).getDate()}`,
49
+ events: [
50
+ {
51
+ title: post.title,
52
+ titleLink: `${base}/#${slugify(post.slugTitle)}`,
53
+ },
54
+ ],
55
+ }))
56
+ );
57
+ </script>
58
+
59
+ {#if contents.length > 1}
60
+ <Block width="normal" class="my-0 relative">
61
+ <div class="table-of-contents" style="--mh: {maxHeight}px;">
62
+ <div class="flex w-full">
63
+ <button
64
+ onclick={() => {
65
+ showContents = !showContents;
66
+ scrollPos = 0;
67
+ }}
68
+ ><div class="icon" class:expanded={showContents}>
69
+ <Fa icon={faCaretDown} size="lg" />
70
+ </div>
71
+ <div
72
+ class="label text-xs uppercase leading-loose tracking-wide py-0.5"
73
+ >
74
+ {label}
75
+ </div></button
76
+ >
77
+ </div>
78
+ <Block
79
+ width="narrow"
80
+ class="my-0 ml-2 relative {showContents ? 'fpb-6' : ''}"
81
+ >
82
+ <div>
83
+ {#if showContents}
84
+ <div
85
+ class="content-container fmt-3"
86
+ transition:slide={{ axis: 'y', duration: 350 }}
87
+ onscroll={(e) => {
88
+ scrollPos = e.currentTarget.scrollTop;
89
+ }}
90
+ >
91
+ <TOCList dates={contents} bind:listHeight />
92
+
93
+ {#if scrollPos > 10 && listHeight > maxHeight}
94
+ <div class="scroll-icon up">
95
+ <Fa icon={faAngleDoubleUp} />
96
+ </div>
97
+ {/if}
98
+
99
+ {#if listHeight > maxHeight && scrollPos < 0.95 * (listHeight - maxHeight)}
100
+ <div class="scroll-icon down">
101
+ <Fa icon={faAngleDoubleDown} />
102
+ </div>
103
+ {/if}
104
+ </div>
105
+ {/if}
106
+ </div></Block
107
+ >
108
+ </div>
109
+ </Block>
110
+ {/if}
111
+
112
+ <style>/* Generated from
113
+ 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
114
+ */
115
+ /* Generated from
116
+ 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
117
+ */
118
+ /* Scales by 1.125 */
119
+ .table-of-contents {
120
+ overflow: hidden;
121
+ margin-bottom: calc(-2 * clamp(0.88rem, 0.83rem + 0.21vw, 1rem));
122
+ }
123
+
124
+ .content-container {
125
+ max-height: var(--mh);
126
+ overflow-y: auto;
127
+ scroll-snap-type: y mandatory;
128
+ }
129
+
130
+ .scroll-icon {
131
+ position: absolute;
132
+ margin-left: -4px;
133
+ color: var(--theme-colour-accent);
134
+ background-color: var(--theme-colour-background);
135
+ border-radius: 50%;
136
+ width: 24px;
137
+ height: 24px;
138
+ display: flex;
139
+ justify-content: center;
140
+ align-items: center;
141
+ }
142
+ .scroll-icon.up {
143
+ top: 0px;
144
+ animation: fade_scroll_up 1.5s ease-in-out infinite;
145
+ }
146
+ .scroll-icon.down {
147
+ bottom: 30px;
148
+ animation: fade_scroll_down 1.5s ease-in-out infinite;
149
+ }
150
+
151
+ @keyframes fade_scroll_up {
152
+ 0% {
153
+ transform: translate(0, 5px);
154
+ opacity: 0;
155
+ }
156
+ 50% {
157
+ opacity: 1;
158
+ }
159
+ 100% {
160
+ transform: translate(0, -10px);
161
+ opacity: 0;
162
+ }
163
+ }
164
+ @keyframes fade_scroll_down {
165
+ 0% {
166
+ transform: translate(0, -10px);
167
+ opacity: 0;
168
+ }
169
+ 50% {
170
+ opacity: 1;
171
+ }
172
+ 100% {
173
+ transform: translate(0, 5px);
174
+ opacity: 0;
175
+ }
176
+ }
177
+ button {
178
+ border: 0;
179
+ background-color: transparent;
180
+ display: inline-flex;
181
+ font-family: var(--theme-font-family-hed);
182
+ font-weight: normal;
183
+ padding: 0;
184
+ color: var(--theme-colour-accent);
185
+ font-size: var(--theme-font-size-sm);
186
+ align-items: center;
187
+ cursor: pointer;
188
+ }
189
+ button div.icon {
190
+ z-index: 1;
191
+ font-size: var(--theme-font-size-sm);
192
+ line-height: 1.7;
193
+ width: 32px;
194
+ height: 32px;
195
+ display: inline-flex;
196
+ justify-content: center;
197
+ align-items: center;
198
+ background: var(--theme-colour-accent);
199
+ color: white;
200
+ border-radius: 50%;
201
+ transition: transform 0.3s ease;
202
+ }
203
+ button div.icon.expanded {
204
+ transform: rotate(180deg);
205
+ }
206
+ button div.label {
207
+ color: var(--theme-colour-accent);
208
+ display: inline-flex;
209
+ font-weight: 500;
210
+ padding-inline-start: clamp(1.13rem, 1.06rem + 0.31vw, 1.31rem);
211
+ padding-inline-end: clamp(0.56rem, 0.52rem + 0.21vw, 0.69rem);
212
+ margin-left: -15px;
213
+ position: relative;
214
+ border-top-right-radius: 20px;
215
+ border-bottom-right-radius: 20px;
216
+ }
217
+ button div.label:after {
218
+ content: "";
219
+ position: absolute;
220
+ top: 0;
221
+ left: 0;
222
+ width: 100%;
223
+ height: 100%;
224
+ opacity: 0.2;
225
+ border-top-right-radius: 20px;
226
+ border-bottom-right-radius: 20px;
227
+ background-color: var(--theme-colour-accent);
228
+ }</style>
@@ -0,0 +1,17 @@
1
+ interface Post {
2
+ title: string;
3
+ slugTitle: string;
4
+ publishTime: string;
5
+ }
6
+ interface Props {
7
+ posts: Post[];
8
+ /** Base path prepended to post links, e.g. "/graphics". */
9
+ base: string;
10
+ /** The label for the table of contents toggle button. */
11
+ label?: string;
12
+ /** The maximum height of the table of contents list in pixels. */
13
+ maxHeight?: number;
14
+ }
15
+ declare const BlogToc: import("svelte").Component<Props, {}, "">;
16
+ type BlogToc = ReturnType<typeof BlogToc>;
17
+ export default BlogToc;
@@ -0,0 +1,130 @@
1
+ <script lang="ts">
2
+ import Block from '../Block/Block.svelte';
3
+
4
+ interface DateEvent {
5
+ title: string;
6
+ titleLink: string;
7
+ }
8
+
9
+ interface DateEntry {
10
+ date: string;
11
+ events: DateEvent[];
12
+ }
13
+
14
+ interface Props {
15
+ dates: DateEntry[];
16
+ /** Colour for the timeline bullet symbols and line. */
17
+ symbolColour?: string;
18
+ /** Colour for the date headings. */
19
+ dateColour?: string;
20
+ /** The height of the list, bindable for the parent to read. */
21
+ listHeight?: number;
22
+ id?: string;
23
+ class?: string;
24
+ }
25
+
26
+ let {
27
+ dates,
28
+ symbolColour = 'var(--theme-colour-brand-rules)',
29
+ dateColour = 'var(--theme-colour-accent, red)',
30
+ listHeight = $bindable(0),
31
+ id = '',
32
+ class: cls = '',
33
+ }: Props = $props();
34
+ </script>
35
+
36
+ <Block width="normal" {id} class="simple-timeline-container {cls}">
37
+ <div
38
+ bind:clientHeight={listHeight}
39
+ class="timeline"
40
+ style="--symbol-colour:{symbolColour};"
41
+ >
42
+ {#each dates as date}
43
+ <div class="date">
44
+ <svg class="absolute bg" height="25" width="20">
45
+ <circle
46
+ cx="10"
47
+ cy="12"
48
+ r="5"
49
+ stroke={symbolColour}
50
+ stroke-width="2"
51
+ fill="transparent"
52
+ ></circle>
53
+ </svg>
54
+ <div class="timeline-date" style:color={dateColour}>
55
+ {date.date}
56
+ </div>
57
+ {#each date.events as event}
58
+ <div class="event">
59
+ <a href={event.titleLink}>
60
+ <div class="title">{event.title}</div>
61
+ </a>
62
+ </div>
63
+ {/each}
64
+ </div>
65
+ {/each}
66
+ </div>
67
+ </Block>
68
+
69
+ <style>/* Generated from
70
+ 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
71
+ */
72
+ /* Generated from
73
+ 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
74
+ */
75
+ /* Scales by 1.125 */
76
+ .timeline {
77
+ padding-left: 0.5rem;
78
+ padding-right: 0.875rem;
79
+ }
80
+ .timeline .date {
81
+ position: relative;
82
+ padding-top: 0.125rem;
83
+ padding-left: 1.25rem;
84
+ padding-bottom: 1rem;
85
+ scroll-snap-align: start;
86
+ border-left: 1px solid var(--symbol-colour);
87
+ }
88
+ .timeline .date:last-child {
89
+ border-left: 1px solid var(--theme-colour-background);
90
+ padding-block-end: 0;
91
+ }
92
+ .timeline .timeline-date {
93
+ font-family: var(--theme-font-family-note);
94
+ font-size: var(--theme-font-size-xs);
95
+ text-transform: uppercase;
96
+ font-weight: 900;
97
+ letter-spacing: 0.03em;
98
+ margin-block-end: 0;
99
+ }
100
+ .timeline svg {
101
+ top: -1px;
102
+ left: -10.5px;
103
+ }
104
+ .timeline div.title {
105
+ color: var(--theme-colour-text-primary);
106
+ font-weight: 600;
107
+ font-family: var(--theme-font-family-subhed);
108
+ line-height: 1.15;
109
+ font-size: var(--theme-font-size-base);
110
+ margin-block-start: clamp(1.69rem, 1.58rem + 0.52vw, 2rem);
111
+ margin-block-end: clamp(0.31rem, 0.31rem + 0vw, 0.31rem);
112
+ margin-block-start: clamp(0.31rem, 0.31rem + 0vw, 0.31rem);
113
+ margin-block-end: clamp(0.31rem, 0.31rem + 0vw, 0.31rem);
114
+ font-weight: 500;
115
+ }
116
+ .timeline div.event a {
117
+ text-decoration: none;
118
+ }
119
+ .timeline div.event a:hover {
120
+ text-decoration: underline;
121
+ }
122
+ .timeline div.event :global(p) {
123
+ margin-block-start: 0;
124
+ margin-block-end: clamp(0.56rem, 0.52rem + 0.21vw, 0.69rem);
125
+ font-family: var(--theme-font-family-note);
126
+ font-size: calc(0.9 * var(--theme-font-size-base));
127
+ color: var(--theme-colour-text-primary);
128
+ line-height: 1.3;
129
+ font-weight: 300;
130
+ }</style>
@@ -0,0 +1,22 @@
1
+ interface DateEvent {
2
+ title: string;
3
+ titleLink: string;
4
+ }
5
+ interface DateEntry {
6
+ date: string;
7
+ events: DateEvent[];
8
+ }
9
+ interface Props {
10
+ dates: DateEntry[];
11
+ /** Colour for the timeline bullet symbols and line. */
12
+ symbolColour?: string;
13
+ /** Colour for the date headings. */
14
+ dateColour?: string;
15
+ /** The height of the list, bindable for the parent to read. */
16
+ listHeight?: number;
17
+ id?: string;
18
+ class?: string;
19
+ }
20
+ declare const TocList: import("svelte").Component<Props, {}, "listHeight">;
21
+ type TocList = ReturnType<typeof TocList>;
22
+ export default TocList;
@@ -13,7 +13,7 @@
13
13
  /**
14
14
  * Publish time as a datetime string.
15
15
  */
16
- publishTime: string;
16
+ publishTime?: string;
17
17
  /**
18
18
  * Update time as a datetime string.
19
19
  */
@@ -52,7 +52,7 @@
52
52
 
53
53
  let {
54
54
  authors = [],
55
- publishTime,
55
+ publishTime = '',
56
56
  updateTime,
57
57
  align = 'auto',
58
58
  id = '',
@@ -7,7 +7,7 @@ interface Props {
7
7
  /**
8
8
  * Publish time as a datetime string.
9
9
  */
10
- publishTime: string;
10
+ publishTime?: string;
11
11
  /**
12
12
  * Update time as a datetime string.
13
13
  */
@@ -0,0 +1,262 @@
1
+ <script lang="ts">
2
+ import { onDestroy, onMount } from 'svelte';
3
+
4
+ const CLOCK_WEIGHT = { Light: 1, Normal: 2, Bold: 4 } as const;
5
+ type ClockWeight = keyof typeof CLOCK_WEIGHT;
6
+
7
+ const CLOCK_SIZE = { XS: 48, MD: 80, LG: 120, XL: 160 } as const;
8
+ type ClockSize = keyof typeof CLOCK_SIZE;
9
+
10
+ interface Props {
11
+ /**
12
+ * The name of the clock (to be displayed), e.g. "New York"
13
+ */
14
+ name: string;
15
+ /**
16
+ * The UTC time to display, defaults to current time
17
+ */
18
+ UTCTime?: Date;
19
+ /**
20
+ * The timezone identifier, e.g. "America/New_York"
21
+ */
22
+ tzIdentifier: string;
23
+ /**
24
+ * Whether to show the clock, defaults to true
25
+ */
26
+ showClock?: boolean;
27
+ /**
28
+ * The weight of the clock, either "normal" or "bold"
29
+ */
30
+ clockWeight?: ClockWeight;
31
+ /**
32
+ * The size of the clock, either "XS", "MD", "LG", or "XL"
33
+ */
34
+ clockSize?: ClockSize;
35
+ }
36
+
37
+ const {
38
+ name,
39
+ UTCTime = new Date(new Date().toUTCString()),
40
+ tzIdentifier,
41
+ showClock = true,
42
+ clockWeight = 'Normal',
43
+ clockSize = 'MD',
44
+ }: Props = $props();
45
+
46
+ /**
47
+ * Converts a UTC date to a specified timezone and formats it to a.m./p.m. style.
48
+ *
49
+ * @param utcDate - The UTC date to convert.
50
+ * @param timezone - The timezone identifier.
51
+ * @returns The formatted time string.
52
+ *
53
+ */
54
+ function convertUTCToTimezone(utcDate: Date, timezone: string) {
55
+ const time = new Date(utcDate).toLocaleString('en-US', {
56
+ timeZone: timezone,
57
+ hour: 'numeric',
58
+ minute: '2-digit',
59
+ hour12: true,
60
+ });
61
+
62
+ // Convert AM/PM to a.m./p.m. format
63
+ return time.replace('AM', 'a.m.').replace('PM', 'p.m.');
64
+ }
65
+
66
+ let clockInterval: ReturnType<typeof setInterval> | null = null;
67
+ let time: string = $state(convertUTCToTimezone(UTCTime, tzIdentifier));
68
+
69
+ onMount(() => {
70
+ clockInterval = setInterval(() => {
71
+ time = convertUTCToTimezone(
72
+ new Date(new Date().toUTCString()),
73
+ tzIdentifier
74
+ );
75
+ }, 1000 * 10); // Update every 10 seconds
76
+ });
77
+
78
+ let minute: number = $derived(
79
+ parseFloat(time?.split(' ')[0].split(':')[1]) || 0
80
+ );
81
+ let hour: number = $derived(
82
+ parseFloat(time?.split(' ')[0].split(':')[0]) || 0
83
+ );
84
+
85
+ onDestroy(() => {
86
+ if (clockInterval) {
87
+ clearInterval(clockInterval);
88
+ clockInterval = null;
89
+ }
90
+ });
91
+ </script>
92
+
93
+ <div class="clock-container" style="--clock-size: {CLOCK_SIZE[clockSize]}px;">
94
+ {#if showClock}
95
+ <svg class="clock-svg" width="100%" height="100%" viewBox="0 0 120 120">
96
+ <defs>
97
+ <filter id="inset-shadow">
98
+ <!-- Shadow offset -->
99
+ <feOffset dx="0" dy="4" />
100
+ <!-- Shadow blur -->
101
+ <feGaussianBlur stdDeviation="8" result="offset-blur" />
102
+ <!-- Invert drop shadow to make an inset shadow-->
103
+ <feComposite
104
+ operator="out"
105
+ in="SourceGraphic"
106
+ in2="offset-blur"
107
+ result="inverse"
108
+ />
109
+ <!-- Cut colour inside shadow -->
110
+ <feFlood flood-color="black" flood-opacity=".2" result="color" />
111
+ <feComposite operator="in" in="color" in2="inverse" result="shadow" />
112
+ <!-- Placing shadow over element -->
113
+ <feComposite operator="over" in="shadow" in2="SourceGraphic" />
114
+ </filter>
115
+ </defs>
116
+ <circle
117
+ class="clock-outer-border"
118
+ cx="50%"
119
+ cy="50%"
120
+ r="58"
121
+ fill="transparent"
122
+ stroke="#cccccc"
123
+ stroke-width="2"
124
+ ></circle>
125
+ <circle
126
+ class="clock-inner-shadow"
127
+ cx="50%"
128
+ cy="50%"
129
+ r="54"
130
+ fill="#ffffff"
131
+ filter="url(#inset-shadow)"
132
+ ></circle>
133
+ <g id="clock-ticks" style="mix-blend-mode: multiply;">
134
+ {#each Array(12) as _, i (i)}
135
+ <line
136
+ class="clock-hour-mark"
137
+ x1="50%"
138
+ y1="56"
139
+ x2="50%"
140
+ y2="64"
141
+ stroke="var(--tr-light-grey)"
142
+ stroke-width="2"
143
+ transform-origin="50% 50%"
144
+ transform="rotate({i * 30}) translate(0, -46)"
145
+ ></line>
146
+ {/each}
147
+ </g>
148
+ <g
149
+ id="clock-hand-minute"
150
+ transform-origin="50% 50%"
151
+ transform="rotate({(minute / 60) * 360})"
152
+ >
153
+ <circle
154
+ cx="50%"
155
+ cy="50%"
156
+ r="4"
157
+ fill="transparent"
158
+ stroke="var(--tr-light-grey)"
159
+ stroke-width={CLOCK_WEIGHT[clockWeight]}
160
+ ></circle>
161
+ <line
162
+ x1="50%"
163
+ y1={60 - 4 - 36}
164
+ x2="50%"
165
+ y2={60 - 4}
166
+ stroke="var(--tr-light-grey)"
167
+ stroke-width={CLOCK_WEIGHT[clockWeight]}
168
+ transform-origin="50% 50%"
169
+ ></line>
170
+ <line
171
+ x1="50%"
172
+ y1={60 + 4}
173
+ x2="50%"
174
+ y2={60 + 4 + 4}
175
+ stroke="var(--tr-light-grey)"
176
+ stroke-width={CLOCK_WEIGHT[clockWeight]}
177
+ transform-origin="50% 50%"
178
+ ></line>
179
+ </g>
180
+ <g
181
+ id="clock-hand-hour"
182
+ transform-origin="50% 50%"
183
+ transform="rotate({(hour / 12) * 360 + (360 / 12) * (minute / 60)})"
184
+ >
185
+ <circle
186
+ cx="50%"
187
+ cy="50%"
188
+ r="4"
189
+ fill="transparent"
190
+ stroke="var(--tr-dark-grey)"
191
+ stroke-width={CLOCK_WEIGHT[clockWeight]}
192
+ ></circle>
193
+ <line
194
+ x1="50%"
195
+ y1={60 - 4 - 24}
196
+ x2="50%"
197
+ y2={60 - 4}
198
+ stroke="var(--tr-dark-grey)"
199
+ stroke-width={CLOCK_WEIGHT[clockWeight]}
200
+ transform-origin="50% 50%"
201
+ ></line>
202
+ <line
203
+ x1="50%"
204
+ y1={60 + 4}
205
+ x2="50%"
206
+ y2={60 + 4 + 4}
207
+ stroke="var(--tr-dark-grey)"
208
+ stroke-width={CLOCK_WEIGHT[clockWeight]}
209
+ transform-origin="50% 50%"
210
+ ></line>
211
+ </g>
212
+ <circle
213
+ class="clock-origin"
214
+ cx="50%"
215
+ cy="50%"
216
+ r="2"
217
+ fill="var(--tr-dark-grey)"
218
+ ></circle>
219
+ </svg>
220
+ {/if}
221
+ <div class="clock-info">
222
+ <p class="m-0 p-0 font-sans font-medium leading-none text-sm">
223
+ {name}
224
+ </p>
225
+ <p
226
+ class="m-0 p-0 font-sans text-xs leading-none"
227
+ style="color: var(--tr-medium-grey);"
228
+ >
229
+ {time}
230
+ </p>
231
+ </div>
232
+ </div>
233
+
234
+ <style>/* Generated from
235
+ 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
236
+ */
237
+ /* Generated from
238
+ 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
239
+ */
240
+ /* Scales by 1.125 */
241
+ .clock-container {
242
+ height: var(--clock-size);
243
+ display: flex;
244
+ justify-content: center;
245
+ align-items: center;
246
+ gap: 8px;
247
+ }
248
+ @media (max-width: 659px) {
249
+ .clock-container {
250
+ height: 48px;
251
+ }
252
+ }
253
+ .clock-container .clock-info {
254
+ display: flex;
255
+ flex-direction: column;
256
+ gap: 2px;
257
+ }
258
+ .clock-container svg {
259
+ aspect-ratio: 1/1;
260
+ width: auto;
261
+ height: 100%;
262
+ }</style>