@reuters-graphics/graphics-components 3.6.0 → 3.8.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/ShareBar/ShareBar.svelte +347 -0
- package/dist/components/ShareBar/ShareBar.svelte.d.ts +21 -0
- package/dist/components/TileMap/TileMap.svelte +31 -0
- package/dist/components/TileMap/TileMap.svelte.d.ts +7 -0
- package/dist/components/TileMap/TileMapLayer.svelte +13 -1
- package/dist/components/TileMap/TileMapLayer.svelte.d.ts +6 -0
- package/dist/components/TileMap/labels.d.ts +34 -0
- package/dist/components/TileMap/labels.js +55 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
<!-- @component `ShareBar` shows a row of share buttons (X, Facebook, LinkedIn, Email, Copy link) and an optional "Purchase Licensing Rights" button, echoing the share toolbar on Reuters.com stories. [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-page-furniture-sharebar--docs) -->
|
|
2
|
+
<script lang="ts">
|
|
3
|
+
import { onDestroy } from 'svelte';
|
|
4
|
+
import Block from '../Block/Block.svelte';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
/**
|
|
8
|
+
* Canonical URL to share. When omitted, the live `window.location.href` is
|
|
9
|
+
* read at click time so a shared link reproduces the reader's current view.
|
|
10
|
+
*/
|
|
11
|
+
url?: string;
|
|
12
|
+
/** Headline used as the share text and email subject. */
|
|
13
|
+
headline?: string;
|
|
14
|
+
/** Destination for the "Purchase Licensing Rights" button. */
|
|
15
|
+
licensingUrl?: string;
|
|
16
|
+
/** Hide the "Purchase Licensing Rights" button when false. */
|
|
17
|
+
showLicensing?: boolean;
|
|
18
|
+
/** ID on the containing block. */
|
|
19
|
+
id?: string;
|
|
20
|
+
/** Extra classes on the containing block. */
|
|
21
|
+
class?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let {
|
|
25
|
+
url,
|
|
26
|
+
headline = '',
|
|
27
|
+
licensingUrl = 'https://www.reutersagency.com/en/licensereuterscontent/?utm_medium=rcom-article-media&utm_campaign=rcom-rcp-lead',
|
|
28
|
+
showLicensing = true,
|
|
29
|
+
id = '',
|
|
30
|
+
class: cls = 'fmy-8',
|
|
31
|
+
}: Props = $props();
|
|
32
|
+
|
|
33
|
+
let copied = $state(false);
|
|
34
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
35
|
+
|
|
36
|
+
/** Resolve the URL to share, falling back to the live page URL. */
|
|
37
|
+
function shareTarget(): string {
|
|
38
|
+
return url ?? (typeof window !== 'undefined' ? window.location.href : '');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Open a centered popup for a social share dialog. */
|
|
42
|
+
function openPopup(shareUrl: string) {
|
|
43
|
+
if (typeof window === 'undefined') return;
|
|
44
|
+
window.open(shareUrl, '_blank', 'noopener,noreferrer,width=600,height=600');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function shareOnX() {
|
|
48
|
+
const u = encodeURIComponent(shareTarget());
|
|
49
|
+
const text = encodeURIComponent(headline);
|
|
50
|
+
openPopup(`https://twitter.com/intent/tweet?url=${u}&text=${text}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function shareOnFacebook() {
|
|
54
|
+
const u = encodeURIComponent(shareTarget());
|
|
55
|
+
openPopup(`https://www.facebook.com/sharer/sharer.php?u=${u}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function shareOnLinkedIn() {
|
|
59
|
+
const u = encodeURIComponent(shareTarget());
|
|
60
|
+
openPopup(`https://www.linkedin.com/sharing/share-offsite/?url=${u}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function shareViaEmail() {
|
|
64
|
+
if (typeof window === 'undefined') return;
|
|
65
|
+
const subject = encodeURIComponent(headline);
|
|
66
|
+
const body = encodeURIComponent(shareTarget());
|
|
67
|
+
window.location.href = `mailto:?subject=${subject}&body=${body}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Legacy clipboard fallback for contexts where the async Clipboard API is
|
|
72
|
+
* unavailable or blocked — most notably inside a cross-origin iframe (how
|
|
73
|
+
* Reuters embeds graphics), where `navigator.clipboard.writeText` rejects
|
|
74
|
+
* without `clipboard-write` permission. Uses a hidden textarea +
|
|
75
|
+
* `document.execCommand('copy')`, which acts on the document's own selection
|
|
76
|
+
* during the click gesture.
|
|
77
|
+
*/
|
|
78
|
+
function legacyCopy(value: string): boolean {
|
|
79
|
+
if (typeof document === 'undefined') return false;
|
|
80
|
+
const textarea = document.createElement('textarea');
|
|
81
|
+
textarea.value = value;
|
|
82
|
+
textarea.setAttribute('readonly', '');
|
|
83
|
+
textarea.style.position = 'fixed';
|
|
84
|
+
textarea.style.top = '-9999px';
|
|
85
|
+
textarea.style.opacity = '0';
|
|
86
|
+
document.body.appendChild(textarea);
|
|
87
|
+
const selection = document.getSelection();
|
|
88
|
+
const previousRange =
|
|
89
|
+
selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
|
|
90
|
+
textarea.select();
|
|
91
|
+
let ok = false;
|
|
92
|
+
try {
|
|
93
|
+
ok = document.execCommand('copy');
|
|
94
|
+
} catch {
|
|
95
|
+
ok = false;
|
|
96
|
+
}
|
|
97
|
+
document.body.removeChild(textarea);
|
|
98
|
+
if (previousRange && selection) {
|
|
99
|
+
selection.removeAllRanges();
|
|
100
|
+
selection.addRange(previousRange);
|
|
101
|
+
}
|
|
102
|
+
return ok;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function showCopied() {
|
|
106
|
+
copied = true;
|
|
107
|
+
if (timer) clearTimeout(timer);
|
|
108
|
+
timer = setTimeout(() => {
|
|
109
|
+
copied = false;
|
|
110
|
+
}, 2000);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function copyLink() {
|
|
114
|
+
const value = shareTarget();
|
|
115
|
+
if (!value) return;
|
|
116
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
117
|
+
try {
|
|
118
|
+
await navigator.clipboard.writeText(value);
|
|
119
|
+
showCopied();
|
|
120
|
+
return;
|
|
121
|
+
} catch {
|
|
122
|
+
// Fall through to the legacy path below.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (legacyCopy(value)) showCopied();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
onDestroy(() => {
|
|
129
|
+
if (timer) clearTimeout(timer);
|
|
130
|
+
});
|
|
131
|
+
</script>
|
|
132
|
+
|
|
133
|
+
<Block {id} class="share-bar-block {cls}" width="normal">
|
|
134
|
+
<div class="share-bar" data-testid="ShareBar" dir="ltr">
|
|
135
|
+
<div class="share-buttons" role="group" aria-label="Share this article">
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
class="icon-button"
|
|
139
|
+
aria-label="Share article on X"
|
|
140
|
+
data-testid="share-x"
|
|
141
|
+
onclick={shareOnX}
|
|
142
|
+
>
|
|
143
|
+
<svg viewBox="0 0 30 30" aria-hidden="true" focusable="false">
|
|
144
|
+
<path
|
|
145
|
+
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"
|
|
146
|
+
/>
|
|
147
|
+
</svg>
|
|
148
|
+
</button>
|
|
149
|
+
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
class="icon-button"
|
|
153
|
+
aria-label="Share article on Facebook"
|
|
154
|
+
data-testid="share-facebook"
|
|
155
|
+
onclick={shareOnFacebook}
|
|
156
|
+
>
|
|
157
|
+
<svg viewBox="6 6 20 20" aria-hidden="true" focusable="false">
|
|
158
|
+
<path
|
|
159
|
+
d="M16 6.04001C10.5 6.04001 6 10.53 6 16.06C6 21.06 9.66 25.21 14.44 25.96V18.96H11.9V16.06H14.44V13.85C14.44 11.34 15.93 9.96001 18.22 9.96001C19.31 9.96001 20.45 10.15 20.45 10.15V12.62H19.19C17.95 12.62 17.56 13.39 17.56 14.18V16.06H20.34L19.89 18.96H17.56V25.96C19.9164 25.5879 22.0622 24.3855 23.6099 22.5701C25.1576 20.7546 26.0054 18.4457 26 16.06C26 10.53 21.5 6.04001 16 6.04001Z"
|
|
160
|
+
/>
|
|
161
|
+
</svg>
|
|
162
|
+
</button>
|
|
163
|
+
|
|
164
|
+
<button
|
|
165
|
+
type="button"
|
|
166
|
+
class="icon-button"
|
|
167
|
+
aria-label="Share article on LinkedIn"
|
|
168
|
+
data-testid="share-linkedin"
|
|
169
|
+
onclick={shareOnLinkedIn}
|
|
170
|
+
>
|
|
171
|
+
<svg viewBox="0 0 21 21" aria-hidden="true" focusable="false">
|
|
172
|
+
<path
|
|
173
|
+
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"
|
|
174
|
+
/>
|
|
175
|
+
</svg>
|
|
176
|
+
</button>
|
|
177
|
+
|
|
178
|
+
<button
|
|
179
|
+
type="button"
|
|
180
|
+
class="icon-button"
|
|
181
|
+
aria-label="Email article"
|
|
182
|
+
data-testid="share-email"
|
|
183
|
+
onclick={shareViaEmail}
|
|
184
|
+
>
|
|
185
|
+
<svg viewBox="0 0 21 18" aria-hidden="true" focusable="false">
|
|
186
|
+
<path
|
|
187
|
+
d="M20.125 9c.449 0 .819.347.87.795L21 9.9v5.4c0 1.438-1.093 2.613-2.47 2.695l-.155.005H2.625c-1.398 0-2.54-1.124-2.62-2.541L0 15.3V9.9c0-.497.392-.9.875-.9.449 0 .819.347.87.795l.005.105v5.4c0 .462.338.842.773.894l.102.006h15.75a.884.884 0 0 0 .87-.795l.005-.105V9.9c0-.497.392-.9.875-.9Zm-1.75-9C19.825 0 21 1.209 21 2.7v2.7c0 .33-.175.632-.456.79l-9.625 5.4a.854.854 0 0 1-.838 0L.456 6.19A.904.904 0 0 1 0 5.4V2.7C0 1.209 1.175 0 2.625 0h15.75Zm0 1.8H2.625a.888.888 0 0 0-.875.9v2.166l8.75 4.909 8.75-4.91V2.7a.892.892 0 0 0-.773-.894l-.102-.006Z"
|
|
188
|
+
/>
|
|
189
|
+
</svg>
|
|
190
|
+
</button>
|
|
191
|
+
|
|
192
|
+
<span class="icon-button-wrap">
|
|
193
|
+
<button
|
|
194
|
+
type="button"
|
|
195
|
+
class="icon-button"
|
|
196
|
+
aria-label="Copy link"
|
|
197
|
+
data-testid="share-link"
|
|
198
|
+
onclick={copyLink}
|
|
199
|
+
>
|
|
200
|
+
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
|
|
201
|
+
<path
|
|
202
|
+
d="M14.47 1.458a4.163 4.163 0 0 0-5.901 0l-1.28 1.209a.688.688 0 0 0 0 .995c.284.285.71.285.995 0l1.21-1.209a2.878 2.878 0 0 1 3.982 0c1.138 1.067 1.137 2.916.07 3.983l-2.133 2.133-.284.284c-1.28.925-3.058.711-3.982-.569-.214-.284-.711-.355-.996-.142-.285.213-.356.71-.142.996a4.242 4.242 0 0 0 3.413 1.706c.853 0 1.777-.284 2.56-.853.142-.142.284-.284.497-.427l2.134-2.133c1.636-1.636 1.564-4.41-.143-5.973Z"
|
|
203
|
+
/>
|
|
204
|
+
<path
|
|
205
|
+
d="m7.644 12.338-1.209 1.209a2.878 2.878 0 0 1-3.982 0c-1.138-1.067-1.138-2.916-.071-3.983l2.133-2.133.284-.284c.64-.427 1.351-.64 2.134-.57.782.143 1.422.498 1.849 1.138.213.284.71.356.996.143.284-.214.355-.711.142-.996-.71-.924-1.707-1.493-2.773-1.636-1.067-.213-2.205.071-3.13.783l-.426.426L1.458 8.57c-1.636 1.706-1.565 4.409.07 6.044.854.782 1.92 1.209 2.987 1.209 1.067 0 2.134-.427 2.987-1.21l1.209-1.208a.688.688 0 0 0 0-.996c-.284-.285-.782-.356-1.067-.07Z"
|
|
206
|
+
/>
|
|
207
|
+
</svg>
|
|
208
|
+
</button>
|
|
209
|
+
{#if copied}
|
|
210
|
+
<span class="tooltip" aria-hidden="true">Link copied</span>
|
|
211
|
+
{/if}
|
|
212
|
+
<span class="visually-hidden" role="status" aria-live="polite">
|
|
213
|
+
{copied ? 'Copied to clipboard' : ''}
|
|
214
|
+
</span>
|
|
215
|
+
</span>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
{#if showLicensing}
|
|
219
|
+
<a
|
|
220
|
+
class="licensing"
|
|
221
|
+
href={licensingUrl}
|
|
222
|
+
target="_blank"
|
|
223
|
+
rel="noopener noreferrer"
|
|
224
|
+
dir="ltr"
|
|
225
|
+
data-testid="licensing-link"
|
|
226
|
+
>
|
|
227
|
+
Purchase Licensing Rights
|
|
228
|
+
</a>
|
|
229
|
+
{/if}
|
|
230
|
+
</div>
|
|
231
|
+
</Block>
|
|
232
|
+
|
|
233
|
+
<style>.share-bar {
|
|
234
|
+
display: flex;
|
|
235
|
+
align-items: center;
|
|
236
|
+
justify-content: space-between;
|
|
237
|
+
flex-wrap: wrap;
|
|
238
|
+
gap: 0.75rem;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.share-buttons {
|
|
242
|
+
display: inline-flex;
|
|
243
|
+
align-items: center;
|
|
244
|
+
gap: 0.5rem;
|
|
245
|
+
flex-wrap: wrap;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.icon-button-wrap {
|
|
249
|
+
position: relative;
|
|
250
|
+
display: inline-flex;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.icon-button {
|
|
254
|
+
width: 2.5rem;
|
|
255
|
+
height: 2.5rem;
|
|
256
|
+
display: inline-flex;
|
|
257
|
+
align-items: center;
|
|
258
|
+
justify-content: center;
|
|
259
|
+
padding: 0;
|
|
260
|
+
border: 1px solid var(--theme-colour-brand-rules, #d3d3d3);
|
|
261
|
+
border-radius: 4px;
|
|
262
|
+
background: var(--theme-colour-background, #fff);
|
|
263
|
+
color: var(--theme-colour-text-secondary, #666);
|
|
264
|
+
cursor: pointer;
|
|
265
|
+
transition: color 0.15s ease, border-color 0.15s ease, background-color 0.15s ease;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.icon-button:hover,
|
|
269
|
+
.icon-button:focus-visible {
|
|
270
|
+
color: var(--theme-colour-text-primary, #121212);
|
|
271
|
+
border-color: var(--theme-colour-text-secondary, #666);
|
|
272
|
+
background: rgba(0, 0, 0, 0.03);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.icon-button svg {
|
|
276
|
+
display: block;
|
|
277
|
+
width: 1.125rem;
|
|
278
|
+
height: 1.125rem;
|
|
279
|
+
fill: currentColor;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.tooltip {
|
|
283
|
+
position: absolute;
|
|
284
|
+
bottom: calc(100% + 0.375rem);
|
|
285
|
+
left: 50%;
|
|
286
|
+
transform: translateX(-50%);
|
|
287
|
+
padding: 0.25rem 0.5rem;
|
|
288
|
+
border-radius: 4px;
|
|
289
|
+
background: var(--theme-colour-text-primary, #121212);
|
|
290
|
+
color: var(--theme-colour-background, #fff);
|
|
291
|
+
font-size: 0.75rem;
|
|
292
|
+
line-height: 1;
|
|
293
|
+
white-space: nowrap;
|
|
294
|
+
pointer-events: none;
|
|
295
|
+
z-index: 1;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.tooltip::after {
|
|
299
|
+
content: "";
|
|
300
|
+
position: absolute;
|
|
301
|
+
top: 100%;
|
|
302
|
+
left: 50%;
|
|
303
|
+
transform: translateX(-50%);
|
|
304
|
+
border: 4px solid transparent;
|
|
305
|
+
border-top-color: var(--theme-colour-text-primary, #121212);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.licensing {
|
|
309
|
+
display: inline-flex;
|
|
310
|
+
align-items: center;
|
|
311
|
+
height: 2.5rem;
|
|
312
|
+
padding: 0 1rem;
|
|
313
|
+
border: 1px solid var(--theme-colour-brand-rules, #d3d3d3);
|
|
314
|
+
border-radius: 4px;
|
|
315
|
+
background: var(--theme-colour-background, #fff);
|
|
316
|
+
color: var(--theme-colour-text-secondary, #666);
|
|
317
|
+
font-size: 0.875rem;
|
|
318
|
+
line-height: 1;
|
|
319
|
+
white-space: nowrap;
|
|
320
|
+
text-decoration: none;
|
|
321
|
+
transition: color 0.15s ease, border-color 0.15s ease, background-color 0.15s ease;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.licensing:hover,
|
|
325
|
+
.licensing:focus-visible {
|
|
326
|
+
color: var(--theme-colour-text-primary, #121212);
|
|
327
|
+
border-color: var(--theme-colour-text-secondary, #666);
|
|
328
|
+
background: rgba(0, 0, 0, 0.03);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.visually-hidden {
|
|
332
|
+
position: absolute;
|
|
333
|
+
width: 1px;
|
|
334
|
+
height: 1px;
|
|
335
|
+
padding: 0;
|
|
336
|
+
margin: -1px;
|
|
337
|
+
overflow: hidden;
|
|
338
|
+
clip: rect(0, 0, 0, 0);
|
|
339
|
+
white-space: nowrap;
|
|
340
|
+
border: 0;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
@media (max-width: 767px) {
|
|
344
|
+
.share-bar {
|
|
345
|
+
align-items: flex-start;
|
|
346
|
+
}
|
|
347
|
+
}</style>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/**
|
|
3
|
+
* Canonical URL to share. When omitted, the live `window.location.href` is
|
|
4
|
+
* read at click time so a shared link reproduces the reader's current view.
|
|
5
|
+
*/
|
|
6
|
+
url?: string;
|
|
7
|
+
/** Headline used as the share text and email subject. */
|
|
8
|
+
headline?: string;
|
|
9
|
+
/** Destination for the "Purchase Licensing Rights" button. */
|
|
10
|
+
licensingUrl?: string;
|
|
11
|
+
/** Hide the "Purchase Licensing Rights" button when false. */
|
|
12
|
+
showLicensing?: boolean;
|
|
13
|
+
/** ID on the containing block. */
|
|
14
|
+
id?: string;
|
|
15
|
+
/** Extra classes on the containing block. */
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
/** `ShareBar` shows a row of share buttons (X, Facebook, LinkedIn, Email, Copy link) and an optional "Purchase Licensing Rights" button, echoing the share toolbar on Reuters.com stories. [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-page-furniture-sharebar--docs) */
|
|
19
|
+
declare const ShareBar: import("svelte").Component<Props, {}, "">;
|
|
20
|
+
type ShareBar = ReturnType<typeof ShareBar>;
|
|
21
|
+
export default ShareBar;
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import GraphicBlock from '../GraphicBlock/GraphicBlock.svelte';
|
|
22
22
|
import type { ContainerWidth } from '../@types/global';
|
|
23
23
|
import type { ProjectionSpecification } from 'maplibre-gl';
|
|
24
|
+
import { emphasizePlaceLabels } from './labels';
|
|
24
25
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
|
25
26
|
|
|
26
27
|
interface Props {
|
|
@@ -65,6 +66,13 @@
|
|
|
65
66
|
* Map style URL
|
|
66
67
|
*/
|
|
67
68
|
styleUrl?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Darken the basemap's place labels and give them a strong white halo, so
|
|
71
|
+
* city/region names stay readable over colored data layers. Applied once on
|
|
72
|
+
* map load; a no-op on styles without `place` labels (e.g. non-default
|
|
73
|
+
* `styleUrl`s), so it degrades gracefully.
|
|
74
|
+
*/
|
|
75
|
+
emphasizeLabels?: boolean;
|
|
68
76
|
/**
|
|
69
77
|
* Map height (default: '500px')
|
|
70
78
|
*/
|
|
@@ -101,6 +109,7 @@
|
|
|
101
109
|
projection,
|
|
102
110
|
interactive = true,
|
|
103
111
|
styleUrl = 'https://graphics.thomsonreuters.com/reuters-protomaps/style.json',
|
|
112
|
+
emphasizeLabels = false,
|
|
104
113
|
height = '500px',
|
|
105
114
|
width = 'normal',
|
|
106
115
|
textWidth = 'normal',
|
|
@@ -155,6 +164,28 @@
|
|
|
155
164
|
);
|
|
156
165
|
}
|
|
157
166
|
|
|
167
|
+
// Darken the basemap's place labels so they read over data layers. Some
|
|
168
|
+
// styles keep applying their own label paint for a moment after the style
|
|
169
|
+
// loads, so a single call at `load` can be overridden and leave the
|
|
170
|
+
// labels their default (lighter) color. Apply on the first `styledata`
|
|
171
|
+
// where the place labels are present, then stop listening.
|
|
172
|
+
if (emphasizeLabels) {
|
|
173
|
+
const applyLabelEmphasis = () => {
|
|
174
|
+
if (!map) return;
|
|
175
|
+
const hasPlaceLabels = map
|
|
176
|
+
.getStyle()
|
|
177
|
+
?.layers?.some(
|
|
178
|
+
(l) =>
|
|
179
|
+
l.type === 'symbol' &&
|
|
180
|
+
(l as { 'source-layer'?: string })['source-layer'] === 'place'
|
|
181
|
+
);
|
|
182
|
+
if (!hasPlaceLabels) return;
|
|
183
|
+
emphasizePlaceLabels(map);
|
|
184
|
+
map.off('styledata', applyLabelEmphasis);
|
|
185
|
+
};
|
|
186
|
+
map.on('styledata', applyLabelEmphasis);
|
|
187
|
+
}
|
|
188
|
+
|
|
158
189
|
// Call the callback when map is ready
|
|
159
190
|
map.on('load', () => {
|
|
160
191
|
if (!map) return;
|
|
@@ -45,6 +45,13 @@ interface Props {
|
|
|
45
45
|
* Map style URL
|
|
46
46
|
*/
|
|
47
47
|
styleUrl?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Darken the basemap's place labels and give them a strong white halo, so
|
|
50
|
+
* city/region names stay readable over colored data layers. Applied once on
|
|
51
|
+
* map load; a no-op on styles without `place` labels (e.g. non-default
|
|
52
|
+
* `styleUrl`s), so it degrades gracefully.
|
|
53
|
+
*/
|
|
54
|
+
emphasizeLabels?: boolean;
|
|
48
55
|
/**
|
|
49
56
|
* Map height (default: '500px')
|
|
50
57
|
*/
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import type { Map as MaplibreMap, GeoJSONSource } from 'maplibre-gl';
|
|
5
5
|
import type { Writable } from 'svelte/store';
|
|
6
6
|
import type { GeoJSON } from 'geojson';
|
|
7
|
+
import { findFirstSymbolLayerId } from './labels';
|
|
7
8
|
|
|
8
9
|
interface Props {
|
|
9
10
|
/**
|
|
@@ -39,6 +40,12 @@
|
|
|
39
40
|
* Layer to insert before (for layer ordering)
|
|
40
41
|
*/
|
|
41
42
|
beforeId?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Insert this layer beneath the basemap's labels, so place names and other
|
|
45
|
+
* symbol layers stay readable on top of it. Ignored when `beforeId` is set
|
|
46
|
+
* (that takes precedence). A no-op on styles with no symbol layers.
|
|
47
|
+
*/
|
|
48
|
+
beneathLabels?: boolean;
|
|
42
49
|
/**
|
|
43
50
|
* Minimum zoom level to display layer
|
|
44
51
|
*/
|
|
@@ -60,6 +67,7 @@
|
|
|
60
67
|
paint = {},
|
|
61
68
|
layout = {},
|
|
62
69
|
beforeId,
|
|
70
|
+
beneathLabels = false,
|
|
63
71
|
minZoom,
|
|
64
72
|
maxZoom,
|
|
65
73
|
filter,
|
|
@@ -146,7 +154,11 @@
|
|
|
146
154
|
if (maxZoom !== undefined) layerConfig.maxzoom = maxZoom;
|
|
147
155
|
if (filter) layerConfig.filter = filter;
|
|
148
156
|
|
|
149
|
-
|
|
157
|
+
// An explicit `beforeId` wins; otherwise `beneathLabels` inserts the
|
|
158
|
+
// layer just below the first symbol (label) layer so labels stay on top.
|
|
159
|
+
const insertBefore =
|
|
160
|
+
beforeId ?? (beneathLabels ? findFirstSymbolLayerId(map) : undefined);
|
|
161
|
+
map.addLayer(layerConfig as never, insertBefore);
|
|
150
162
|
}
|
|
151
163
|
|
|
152
164
|
isInitialized = true;
|
|
@@ -24,6 +24,12 @@ interface Props {
|
|
|
24
24
|
* Layer to insert before (for layer ordering)
|
|
25
25
|
*/
|
|
26
26
|
beforeId?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Insert this layer beneath the basemap's labels, so place names and other
|
|
29
|
+
* symbol layers stay readable on top of it. Ignored when `beforeId` is set
|
|
30
|
+
* (that takes precedence). A no-op on styles with no symbol layers.
|
|
31
|
+
*/
|
|
32
|
+
beneathLabels?: boolean;
|
|
27
33
|
/**
|
|
28
34
|
* Minimum zoom level to display layer
|
|
29
35
|
*/
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Map as MaplibreMap } from 'maplibre-gl';
|
|
2
|
+
/**
|
|
3
|
+
* Find the first symbol (label) layer in the map's current style, or undefined.
|
|
4
|
+
* Symbol layers are the basemap's place names, road labels, etc. Inserting a
|
|
5
|
+
* data layer *before* this id keeps every label above the data.
|
|
6
|
+
*
|
|
7
|
+
* @see https://docs.mapbox.com/mapbox-gl-js/example/geojson-layer-in-stack/
|
|
8
|
+
*/
|
|
9
|
+
export declare function findFirstSymbolLayerId(map: MaplibreMap): string | undefined;
|
|
10
|
+
/** Options for {@link emphasizePlaceLabels}. */
|
|
11
|
+
export interface EmphasizeLabelsOptions {
|
|
12
|
+
/** Text color for place labels. Default `#1a1a1a` (near-black). */
|
|
13
|
+
textColor?: string;
|
|
14
|
+
/** Halo color. Default solid white. */
|
|
15
|
+
haloColor?: string;
|
|
16
|
+
/** Halo width, in px. Default `1.6`. */
|
|
17
|
+
haloWidth?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Darken the basemap's populated-place labels (cities, towns, capitals, …) and
|
|
21
|
+
* give them a strong white halo so they stay legible on top of colored data
|
|
22
|
+
* overlays.
|
|
23
|
+
*
|
|
24
|
+
* The default protomaps style fades place labels with a zoom-driven
|
|
25
|
+
* `text-opacity` (city labels only reach full strength around zoom 9), so this
|
|
26
|
+
* also pins `text-opacity` to 1 — otherwise the near-black color reads as gray
|
|
27
|
+
* at the low-to-mid zooms typical of a choropleth.
|
|
28
|
+
*
|
|
29
|
+
* Targets the `place` source-layer symbol layers of the default Reuters
|
|
30
|
+
* protomaps style. On a style that doesn't have them it's a **no-op** (the
|
|
31
|
+
* per-layer checks and `try/catch` make it degrade gracefully), so it's safe to
|
|
32
|
+
* call regardless of `styleUrl`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function emphasizePlaceLabels(map: MaplibreMap, opts?: EmphasizeLabelsOptions): void;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find the first symbol (label) layer in the map's current style, or undefined.
|
|
3
|
+
* Symbol layers are the basemap's place names, road labels, etc. Inserting a
|
|
4
|
+
* data layer *before* this id keeps every label above the data.
|
|
5
|
+
*
|
|
6
|
+
* @see https://docs.mapbox.com/mapbox-gl-js/example/geojson-layer-in-stack/
|
|
7
|
+
*/
|
|
8
|
+
export function findFirstSymbolLayerId(map) {
|
|
9
|
+
const layers = map.getStyle()?.layers;
|
|
10
|
+
if (!layers)
|
|
11
|
+
return undefined;
|
|
12
|
+
for (const layer of layers) {
|
|
13
|
+
if (layer.type === 'symbol')
|
|
14
|
+
return layer.id;
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Darken the basemap's populated-place labels (cities, towns, capitals, …) and
|
|
20
|
+
* give them a strong white halo so they stay legible on top of colored data
|
|
21
|
+
* overlays.
|
|
22
|
+
*
|
|
23
|
+
* The default protomaps style fades place labels with a zoom-driven
|
|
24
|
+
* `text-opacity` (city labels only reach full strength around zoom 9), so this
|
|
25
|
+
* also pins `text-opacity` to 1 — otherwise the near-black color reads as gray
|
|
26
|
+
* at the low-to-mid zooms typical of a choropleth.
|
|
27
|
+
*
|
|
28
|
+
* Targets the `place` source-layer symbol layers of the default Reuters
|
|
29
|
+
* protomaps style. On a style that doesn't have them it's a **no-op** (the
|
|
30
|
+
* per-layer checks and `try/catch` make it degrade gracefully), so it's safe to
|
|
31
|
+
* call regardless of `styleUrl`.
|
|
32
|
+
*/
|
|
33
|
+
export function emphasizePlaceLabels(map, opts = {}) {
|
|
34
|
+
const { textColor = '#1a1a1a', haloColor = 'rgba(255, 255, 255, 1)', haloWidth = 1.6, } = opts;
|
|
35
|
+
const layers = map.getStyle()?.layers;
|
|
36
|
+
if (!layers)
|
|
37
|
+
return;
|
|
38
|
+
for (const layer of layers) {
|
|
39
|
+
if (layer.type !== 'symbol')
|
|
40
|
+
continue;
|
|
41
|
+
const sourceLayer = layer['source-layer'];
|
|
42
|
+
if (sourceLayer !== 'place')
|
|
43
|
+
continue;
|
|
44
|
+
try {
|
|
45
|
+
map.setPaintProperty(layer.id, 'text-color', textColor);
|
|
46
|
+
map.setPaintProperty(layer.id, 'text-halo-color', haloColor);
|
|
47
|
+
map.setPaintProperty(layer.id, 'text-halo-width', haloWidth);
|
|
48
|
+
map.setPaintProperty(layer.id, 'text-halo-blur', 0);
|
|
49
|
+
map.setPaintProperty(layer.id, 'text-opacity', 1);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
/* layer isn't paintable in this style; skip it */
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -46,6 +46,7 @@ export { default as ReutersLogo } from './components/ReutersLogo/ReutersLogo.sve
|
|
|
46
46
|
export { default as Scroller } from './components/Scroller/Scroller.svelte';
|
|
47
47
|
export { default as SearchInput } from './components/SearchInput/SearchInput.svelte';
|
|
48
48
|
export { default as SEO } from './components/SEO/SEO.svelte';
|
|
49
|
+
export { default as ShareBar } from './components/ShareBar/ShareBar.svelte';
|
|
49
50
|
export { default as SimpleTimeline } from './components/SimpleTimeline/SimpleTimeline.svelte';
|
|
50
51
|
export { default as SiteFooter } from './components/SiteFooter/SiteFooter.svelte';
|
|
51
52
|
export { default as SiteHeader } from './components/SiteHeader/SiteHeader.svelte';
|
package/dist/index.js
CHANGED
|
@@ -49,6 +49,7 @@ export { default as ReutersLogo } from './components/ReutersLogo/ReutersLogo.sve
|
|
|
49
49
|
export { default as Scroller } from './components/Scroller/Scroller.svelte';
|
|
50
50
|
export { default as SearchInput } from './components/SearchInput/SearchInput.svelte';
|
|
51
51
|
export { default as SEO } from './components/SEO/SEO.svelte';
|
|
52
|
+
export { default as ShareBar } from './components/ShareBar/ShareBar.svelte';
|
|
52
53
|
export { default as SimpleTimeline } from './components/SimpleTimeline/SimpleTimeline.svelte';
|
|
53
54
|
export { default as SiteFooter } from './components/SiteFooter/SiteFooter.svelte';
|
|
54
55
|
export { default as SiteHeader } from './components/SiteHeader/SiteHeader.svelte';
|