@reuters-graphics/graphics-components 3.2.1 → 3.3.1

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,270 @@
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 = 'Show all articles',
34
+ maxHeight = 300,
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
+ .reduce(
48
+ (acc, post) => {
49
+ const d = new Date(post.publishTime);
50
+ const dateKey = `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
51
+ const existing = acc.find((entry) => entry.dateKey === dateKey);
52
+ const event = {
53
+ title: post.title,
54
+ titleLink: `${base}/#${slugify(post.slugTitle)}`,
55
+ };
56
+ if (existing) {
57
+ existing.events.push(event);
58
+ } else {
59
+ acc.push({
60
+ dateKey,
61
+ date: `${apmonth(d)} ${d.getDate()}`,
62
+ events: [event],
63
+ });
64
+ }
65
+ return acc;
66
+ },
67
+ [] as {
68
+ dateKey: string;
69
+ date: string;
70
+ events: { title: string; titleLink: string }[];
71
+ }[]
72
+ )
73
+ .map(({ dateKey: _dateKey, ...rest }) => rest)
74
+ );
75
+ </script>
76
+
77
+ {#if contents.length > 1}
78
+ <Block width="fluid" class="mt-4 mb-0">
79
+ <div class="toc-gutter">
80
+ <div class="table-of-contents" style="--mh: {maxHeight}px;">
81
+ <div class="flex w-full">
82
+ <button
83
+ onclick={() => {
84
+ showContents = !showContents;
85
+ scrollPos = 0;
86
+ }}
87
+ >
88
+ <div class="icon" class:expanded={showContents}>
89
+ <Fa icon={faCaretDown} size="lg" />
90
+ </div>
91
+ <div class="label text-xs leading-loose tracking-wide py-0.5">
92
+ {label}
93
+ </div>
94
+ </button>
95
+ </div>
96
+ <Block
97
+ width="normal"
98
+ class="my-0 ml-2 relative {showContents ? 'fpb-6' : ''}"
99
+ >
100
+ <div>
101
+ {#if showContents}
102
+ <div
103
+ class="content-container fmt-3"
104
+ transition:slide={{ axis: 'y', duration: 350 }}
105
+ onscroll={(e) => {
106
+ scrollPos = e.currentTarget.scrollTop;
107
+ }}
108
+ >
109
+ <TOCList dates={contents} bind:listHeight />
110
+
111
+ {#if scrollPos > 10 && listHeight > maxHeight}
112
+ <div class="scroll-icon up">
113
+ <Fa icon={faAngleDoubleUp} />
114
+ </div>
115
+ {/if}
116
+
117
+ {#if listHeight > maxHeight && scrollPos < 0.95 * (listHeight - maxHeight)}
118
+ <div class="scroll-icon down">
119
+ <Fa icon={faAngleDoubleDown} />
120
+ </div>
121
+ {/if}
122
+ </div>
123
+ {/if}
124
+ </div></Block
125
+ >
126
+ </div>
127
+ </div>
128
+ </Block>
129
+ {/if}
130
+
131
+ <style>/* Generated from
132
+ 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
133
+ */
134
+ /* Generated from
135
+ 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
136
+ */
137
+ /* Scales by 1.125 */
138
+ .toc-gutter {
139
+ padding: 5px 0 0;
140
+ margin-bottom: -40px;
141
+ }
142
+
143
+ .table-of-contents {
144
+ overflow: hidden;
145
+ max-width: var(--normal-column-width);
146
+ margin-left: auto;
147
+ margin-right: auto;
148
+ }
149
+ .table-of-contents > div {
150
+ display: flex;
151
+ justify-content: center;
152
+ }
153
+
154
+ .content-container {
155
+ max-height: var(--mh);
156
+ overflow-y: auto;
157
+ scroll-snap-type: y mandatory;
158
+ }
159
+
160
+ .scroll-icon {
161
+ position: absolute;
162
+ margin-left: -4px;
163
+ color: var(--theme-colour-text-secondary);
164
+ background-color: var(--theme-colour-background);
165
+ border-radius: 50%;
166
+ width: 24px;
167
+ height: 24px;
168
+ display: flex;
169
+ justify-content: center;
170
+ align-items: center;
171
+ }
172
+ .scroll-icon.up {
173
+ top: 0px;
174
+ animation: fade_scroll_up 1.5s ease-in-out infinite;
175
+ }
176
+ .scroll-icon.down {
177
+ bottom: 30px;
178
+ animation: fade_scroll_down 1.5s ease-in-out infinite;
179
+ }
180
+
181
+ @keyframes fade_scroll_up {
182
+ 0% {
183
+ transform: translate(0, 5px);
184
+ opacity: 0;
185
+ }
186
+ 50% {
187
+ opacity: 1;
188
+ }
189
+ 100% {
190
+ transform: translate(0, -10px);
191
+ opacity: 0;
192
+ }
193
+ }
194
+ @keyframes fade_scroll_down {
195
+ 0% {
196
+ transform: translate(0, -10px);
197
+ opacity: 0;
198
+ }
199
+ 50% {
200
+ opacity: 1;
201
+ }
202
+ 100% {
203
+ transform: translate(0, 5px);
204
+ opacity: 0;
205
+ }
206
+ }
207
+ button {
208
+ border: 0;
209
+ background-color: transparent;
210
+ display: inline-flex;
211
+ font-family: var(--theme-font-family-hed);
212
+ font-weight: normal;
213
+ padding: 0 5px;
214
+ color: var(--theme-colour-accent);
215
+ font-size: var(--theme-font-size-sm);
216
+ align-items: center;
217
+ cursor: pointer;
218
+ border: 1px solid var(--tr-muted-grey);
219
+ border-radius: 50px;
220
+ }
221
+ button:hover {
222
+ border: 1px solid var(--theme-colour-text-secondary);
223
+ background-color: #efefef;
224
+ }
225
+ button:hover div.label {
226
+ color: var(--theme-colour-text-primary);
227
+ }
228
+ button:hover div.icon {
229
+ color: var(--theme-colour-text-secondary);
230
+ }
231
+ button div.icon {
232
+ z-index: 1;
233
+ font-size: var(--theme-font-size-sm);
234
+ line-height: 1.7;
235
+ width: 18px;
236
+ height: 24px;
237
+ margin-left: 6px;
238
+ display: inline-flex;
239
+ justify-content: center;
240
+ align-items: center;
241
+ color: var(--tr-light-grey);
242
+ border-radius: 50%;
243
+ transition: transform 0.3s ease;
244
+ }
245
+ button div.icon.expanded {
246
+ transform: rotate(180deg);
247
+ }
248
+ button div.label {
249
+ color: var(--theme-colour-text-secondary);
250
+ display: inline-flex;
251
+ font-weight: 500;
252
+ padding-inline-start: clamp(1.13rem, 1.06rem + 0.31vw, 1.31rem);
253
+ padding-inline-end: clamp(0.56rem, 0.52rem + 0.21vw, 0.69rem);
254
+ margin-left: -15px;
255
+ position: relative;
256
+ border-top-right-radius: 20px;
257
+ border-bottom-right-radius: 20px;
258
+ font-style: italic;
259
+ }
260
+ button div.label:after {
261
+ content: "";
262
+ position: absolute;
263
+ top: 0;
264
+ left: 0;
265
+ width: 100%;
266
+ height: 100%;
267
+ opacity: 0.2;
268
+ border-top-right-radius: 20px;
269
+ border-bottom-right-radius: 20px;
270
+ }</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,134 @@
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-xxs);
95
+ text-transform: uppercase;
96
+ font-weight: 600;
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
+ font-size: var(--theme-font-size-base);
113
+ margin-block-start: clamp(0.31rem, 0.31rem + 0vw, 0.31rem);
114
+ margin-block-end: clamp(0.31rem, 0.31rem + 0vw, 0.31rem);
115
+ font-weight: 500;
116
+ }
117
+ .timeline div.event {
118
+ margin-bottom: 0.75rem;
119
+ }
120
+ .timeline div.event a {
121
+ text-decoration: none;
122
+ }
123
+ .timeline div.event a:hover {
124
+ text-decoration: underline;
125
+ }
126
+ .timeline div.event :global(p) {
127
+ margin-block-start: 0;
128
+ margin-block-end: clamp(0.56rem, 0.52rem + 0.21vw, 0.69rem);
129
+ font-family: var(--theme-font-family-note);
130
+ font-size: calc(0.9 * var(--theme-font-size-base));
131
+ color: var(--theme-colour-text-primary);
132
+ line-height: 1.3;
133
+ font-weight: 300;
134
+ }</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
  */