@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.
- package/dist/components/BlogPost/BlogPost.svelte +101 -0
- package/dist/components/BlogPost/BlogPost.svelte.d.ts +33 -0
- package/dist/components/BlogPost/CopyLink.svelte +131 -0
- package/dist/components/BlogPost/CopyLink.svelte.d.ts +17 -0
- package/dist/components/BlogPost/PostHeadline.svelte +184 -0
- package/dist/components/BlogPost/PostHeadline.svelte.d.ts +26 -0
- package/dist/components/BlogPost/utils.d.ts +6 -0
- package/dist/components/BlogPost/utils.js +6 -0
- package/dist/components/BlogTOC/BlogTOC.svelte +228 -0
- package/dist/components/BlogTOC/BlogTOC.svelte.d.ts +17 -0
- package/dist/components/BlogTOC/TOCList.svelte +130 -0
- package/dist/components/BlogTOC/TOCList.svelte.d.ts +22 -0
- package/dist/components/Byline/Byline.svelte +2 -2
- package/dist/components/Byline/Byline.svelte.d.ts +1 -1
- package/dist/components/ClockWall/Clock.svelte +262 -0
- package/dist/components/ClockWall/Clock.svelte.d.ts +28 -0
- package/dist/components/ClockWall/ClockWall.svelte +65 -0
- package/dist/components/ClockWall/ClockWall.svelte.d.ts +17 -0
- package/dist/components/KinesisLogo/KinesisLogo.svelte +140 -0
- package/dist/components/KinesisLogo/KinesisLogo.svelte.d.ts +10 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/journalize.d.ts +9 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/index.js +9 -2
- package/package.json +1 -1
|
@@ -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
|
|
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 = '',
|
|
@@ -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>
|