@swr-data-lab/components 1.0.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/.storybook/main.ts +20 -0
- package/.storybook/preview.ts +14 -0
- package/package.json +52 -0
- package/src/Autocomplete/Autocomplete.stories.js +68 -0
- package/src/Autocomplete/Autocomplete.svelte +232 -0
- package/src/Autocomplete/index.js +2 -0
- package/src/Intro.mdx +7 -0
- package/src/Switcher/Switcher.stories.js +38 -0
- package/src/Switcher/Switcher.svelte +110 -0
- package/src/Switcher/index.js +2 -0
- package/src/app.d.ts +13 -0
- package/src/app.html +12 -0
- package/src/assets/icons/close-circle.svg.svelte +3 -0
- package/src/assets/icons/grow.svg.svelte +3 -0
- package/src/assets/icons/hand-pointer.svg.svelte +3 -0
- package/src/assets/icons/shrink.svg.svelte +3 -0
- package/src/assets/icons/tap.svg.svelte +31 -0
- package/src/assets/icons/times-circle-solid.svg.svelte +1 -0
- package/src/events/clickOutside.js +23 -0
- package/src/styles/css/colors.css +77 -0
- package/src/styles/scss/_colors.scss +87 -0
- package/src/styles/scss/_constants.scss +43 -0
- package/src/styles/scss/_functions.scss +30 -0
- package/src/styles/scss/_typography.scss +49 -0
- package/src/styles/scss/base.scss +10 -0
- package/src/utils/formatDate.js +17 -0
- package/src/utils/formatNumber.js +13 -0
- package/src/utils/getColorsBetween.js +25 -0
- package/src/utils/getComparisonDiffs.js +9 -0
- package/src/utils/getEmbedContext.js +28 -0
- package/src/utils/getLaenderFromTopo.js +35 -0
- package/src/utils/getLaenderNicenameFromAgs.js +27 -0
- package/src/utils/isSvelteComponent.js +13 -0
- package/src/utils/prepareSophoraModel.js +31 -0
- package/src/utils/scrollIntoViewWithOffset.js +14 -0
- package/src/utils/topoToGeo.js +13 -0
- package/static/favicon.png +0 -0
- package/svelte.config.js +18 -0
- package/tsconfig.json +19 -0
- package/vite.config.ts +6 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { StorybookConfig } from "@storybook/sveltekit";
|
|
2
|
+
|
|
3
|
+
const config: StorybookConfig = {
|
|
4
|
+
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|ts|svelte)"],
|
|
5
|
+
addons: [
|
|
6
|
+
"@storybook/addon-svelte-csf",
|
|
7
|
+
"@storybook/addon-links",
|
|
8
|
+
"@storybook/addon-essentials",
|
|
9
|
+
"@chromatic-com/storybook",
|
|
10
|
+
"@storybook/addon-interactions",
|
|
11
|
+
],
|
|
12
|
+
framework: {
|
|
13
|
+
name: "@storybook/sveltekit",
|
|
14
|
+
options: {},
|
|
15
|
+
},
|
|
16
|
+
core: {
|
|
17
|
+
builder: '@storybook/builder-vite'
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
export default config;
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@swr-data-lab/components",
|
|
3
|
+
"description": "SWR Data Lab component library",
|
|
4
|
+
"author": "SWR Data Lab",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "restricted"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "vite dev",
|
|
11
|
+
"build": "vite build",
|
|
12
|
+
"preview": "vite preview",
|
|
13
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
14
|
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
15
|
+
"storybook": "storybook dev -p 6006",
|
|
16
|
+
"start": "storybook dev -p 6006",
|
|
17
|
+
"build-storybook": "storybook build --disable-telemetry",
|
|
18
|
+
"test-storybook": "test-storybook --browsers chromium firefox",
|
|
19
|
+
"test-storybook:ci": "concurrently -k -s first -n \"Storybook,Test\" -c \"magenta,blue\" \"npm run build-storybook --quiet && npx http-server storybook-static --port 6006 --silent\" \"wait-on tcp:6006 && npm run test-storybook\"",
|
|
20
|
+
"semantic-release": "semantic-release"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@chromatic-com/storybook": "^2.0.2",
|
|
24
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
25
|
+
"@semantic-release/git": "^10.0.1",
|
|
26
|
+
"@semantic-release/npm": "^12.0.1",
|
|
27
|
+
"@storybook/addon-essentials": "^8.3.0",
|
|
28
|
+
"@storybook/addon-interactions": "^8.3.0",
|
|
29
|
+
"@storybook/addon-links": "^8.3.0",
|
|
30
|
+
"@storybook/addon-svelte-csf": "^4.1.7",
|
|
31
|
+
"@storybook/blocks": "^8.3.0",
|
|
32
|
+
"@storybook/builder-vite": "^8.3.0",
|
|
33
|
+
"@storybook/svelte": "^8.3.0",
|
|
34
|
+
"@storybook/sveltekit": "^8.3.0",
|
|
35
|
+
"@storybook/test": "^8.3.0",
|
|
36
|
+
"@storybook/test-runner": "^0.19.1",
|
|
37
|
+
"@sveltejs/adapter-auto": "^3.0.0",
|
|
38
|
+
"@sveltejs/kit": "^2.0.0",
|
|
39
|
+
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
|
40
|
+
"concurrently": "^9.0.1",
|
|
41
|
+
"http-server": "^14.1.1",
|
|
42
|
+
"sass-embedded": "^1.78.0",
|
|
43
|
+
"semantic-release": "^24.1.2",
|
|
44
|
+
"storybook": "^8.3.0",
|
|
45
|
+
"svelte": "^4.2.7",
|
|
46
|
+
"svelte-check": "^4.0.0",
|
|
47
|
+
"typescript": "^5.0.0",
|
|
48
|
+
"vite": "^5.0.3",
|
|
49
|
+
"wait-on": "^8.0.1"
|
|
50
|
+
},
|
|
51
|
+
"type": "module"
|
|
52
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { userEvent, within, expect, fn } from "@storybook/test"
|
|
2
|
+
import AutoComplete from "."
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
title: "Example/Autocomplete",
|
|
6
|
+
tags: ["autodocs"],
|
|
7
|
+
component: AutoComplete,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const testData = ["Apples", "Oranges", "Pears", "Peaches", "Bananas"].map((el) => {
|
|
11
|
+
return {
|
|
12
|
+
value: el,
|
|
13
|
+
label: el,
|
|
14
|
+
details: {},
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
export const Basic = {
|
|
19
|
+
args: {
|
|
20
|
+
data: testData,
|
|
21
|
+
query: "",
|
|
22
|
+
placeholder: "Select a fruit",
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const onSelect = fn((e) => {
|
|
27
|
+
return e.detail
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
export const Test = {
|
|
31
|
+
parameters: {
|
|
32
|
+
docs: {
|
|
33
|
+
story: {
|
|
34
|
+
autoplay: true,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
args: {
|
|
39
|
+
data: testData,
|
|
40
|
+
query: "",
|
|
41
|
+
placeholder: "Select a fruit",
|
|
42
|
+
event_select: onSelect,
|
|
43
|
+
},
|
|
44
|
+
play: async ({ args, canvasElement, step }) => {
|
|
45
|
+
const canvas = within(canvasElement)
|
|
46
|
+
const input = canvas.getByTestId("autocomplete-input")
|
|
47
|
+
await step("Select using the mouse", async () => {
|
|
48
|
+
await userEvent.click(input)
|
|
49
|
+
expect(input).toHaveFocus()
|
|
50
|
+
await userEvent.keyboard("ba")
|
|
51
|
+
const bananasOption = canvas.getByText("Bananas")
|
|
52
|
+
await userEvent.click(bananasOption)
|
|
53
|
+
expect(input).toHaveValue("Bananas")
|
|
54
|
+
expect(onSelect).toHaveReturnedWith({ item: { label: "Bananas", value: "Bananas", details: {} } })
|
|
55
|
+
})
|
|
56
|
+
await userEvent.clear(input)
|
|
57
|
+
await step("Select using the keyboard", async () => {
|
|
58
|
+
await userEvent.click(input)
|
|
59
|
+
expect(input).toHaveFocus()
|
|
60
|
+
await userEvent.keyboard("ap")
|
|
61
|
+
await userEvent.keyboard("{ArrowDown}")
|
|
62
|
+
await userEvent.keyboard("{Enter}")
|
|
63
|
+
expect(input).toHaveValue("Apples")
|
|
64
|
+
expect(onSelect).toHaveReturnedWith({ item: { label: "Apples", value: "Apples", details: {} } })
|
|
65
|
+
})
|
|
66
|
+
await userEvent.clear(input)
|
|
67
|
+
},
|
|
68
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { createEventDispatcher } from 'svelte';
|
|
3
|
+
import Circle from '../assets/icons/times-circle-solid.svg.svelte';
|
|
4
|
+
|
|
5
|
+
export let data = [];
|
|
6
|
+
export let query = '';
|
|
7
|
+
export let placeholder = 'Platzhalter';
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
const sortData = (a, b) => {
|
|
11
|
+
return a.value.localeCompare(b.value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let listRef;
|
|
15
|
+
let controlsRef;
|
|
16
|
+
let inputRef;
|
|
17
|
+
const dispatch = createEventDispatcher();
|
|
18
|
+
|
|
19
|
+
let highlightedItemIndex = -1;
|
|
20
|
+
$: suggestions = data.sort(sortData).slice(0, 50);
|
|
21
|
+
$: isActive = false;
|
|
22
|
+
|
|
23
|
+
// Insert clicked item into search input,
|
|
24
|
+
// dispatch it as select event and close the dropdown
|
|
25
|
+
const handleItemClick = (index) => {
|
|
26
|
+
highlightedItemIndex = index;
|
|
27
|
+
setSelectedItem(suggestions[highlightedItemIndex]);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const setSelectedItem = (item) => {
|
|
31
|
+
query = item.value;
|
|
32
|
+
isActive = false;
|
|
33
|
+
dispatch('select', { item });
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Register keyboard events
|
|
37
|
+
const handleKeyDown = (e) => {
|
|
38
|
+
if (e.key === 'ArrowDown') {
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
highlightedItemIndex =
|
|
41
|
+
highlightedItemIndex < suggestions.length - 1 ? highlightedItemIndex + 1 : 0;
|
|
42
|
+
const target = listRef.querySelector(`ul li:nth-child(${highlightedItemIndex + 1})`);
|
|
43
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
44
|
+
} else if (e.key === 'ArrowUp') {
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
highlightedItemIndex =
|
|
47
|
+
highlightedItemIndex > 0 ? highlightedItemIndex - 1 : suggestions.length - 1;
|
|
48
|
+
const target = listRef.querySelector(`ul li:nth-child(${highlightedItemIndex + 1})`);
|
|
49
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
50
|
+
} else if (e.key === 'Enter') {
|
|
51
|
+
if (highlightedItemIndex > -1){
|
|
52
|
+
setSelectedItem(suggestions[highlightedItemIndex]);
|
|
53
|
+
isActive = false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const handleClear = (e) => {
|
|
59
|
+
query = '';
|
|
60
|
+
inputRef.focus()
|
|
61
|
+
setSuggestions()
|
|
62
|
+
dispatch('select', {
|
|
63
|
+
item: null
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
const setSuggestions = () => {
|
|
69
|
+
suggestions = data.filter((el) =>{
|
|
70
|
+
return el.value.toLowerCase().startsWith(query.toLowerCase())
|
|
71
|
+
}).sort(sortData);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// update dropdown list if input value changes
|
|
75
|
+
const handleInput = () => {
|
|
76
|
+
isActive = true;
|
|
77
|
+
if (query.length === 0){
|
|
78
|
+
suggestions = data
|
|
79
|
+
} else {
|
|
80
|
+
setSuggestions()
|
|
81
|
+
highlightedItemIndex = -1;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
</script>
|
|
85
|
+
|
|
86
|
+
<!--
|
|
87
|
+
This component is used to display a list of suggestions for a given query. If an item is clicked or the enter key is pressed, the selected item will be returned by a custom select event.
|
|
88
|
+
|
|
89
|
+
Data should be provided as array of objects. Each object contains the information for a dropdown entry:
|
|
90
|
+
- **value** *string* the text that is displayed in the input. Has to be unique!
|
|
91
|
+
- **label** *string* | *svelte component* | *array of svelte component and props object* text, html or component displayed in the dropdown list
|
|
92
|
+
- **details** *object* extra data needed by selection handler
|
|
93
|
+
@component
|
|
94
|
+
-->
|
|
95
|
+
|
|
96
|
+
<div class="autocomplete">
|
|
97
|
+
<input
|
|
98
|
+
type="text"
|
|
99
|
+
{placeholder}
|
|
100
|
+
autocomplete="off"
|
|
101
|
+
autocorrect="off"
|
|
102
|
+
autocapitalize="off"
|
|
103
|
+
data-testid="autocomplete-input"
|
|
104
|
+
bind:this={inputRef}
|
|
105
|
+
on:keydown={handleKeyDown}
|
|
106
|
+
bind:value={query}
|
|
107
|
+
on:input={handleInput}
|
|
108
|
+
on:focus={() => {
|
|
109
|
+
isActive = true;
|
|
110
|
+
}}
|
|
111
|
+
on:blur={(e) => {
|
|
112
|
+
if (listRef.contains(e.relatedTarget)){
|
|
113
|
+
window.setTimeout(()=>{
|
|
114
|
+
isActive = false;
|
|
115
|
+
}, 100)
|
|
116
|
+
} else if (!controlsRef.contains(e.relatedTarget)){
|
|
117
|
+
isActive = false
|
|
118
|
+
}
|
|
119
|
+
}}
|
|
120
|
+
/>
|
|
121
|
+
|
|
122
|
+
<ul tabindex="-1" bind:this={listRef} class:active={isActive}>
|
|
123
|
+
{#each suggestions as item, i (item.value)}
|
|
124
|
+
<li>
|
|
125
|
+
<button
|
|
126
|
+
class={`item ${i === highlightedItemIndex ? 'active' : ''}`}
|
|
127
|
+
tabindex="-1"
|
|
128
|
+
type="button"
|
|
129
|
+
on:click={(e) => {
|
|
130
|
+
e.preventDefault();
|
|
131
|
+
handleItemClick(i);
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
{item.label}
|
|
135
|
+
</button>
|
|
136
|
+
</li>
|
|
137
|
+
{/each}
|
|
138
|
+
</ul>
|
|
139
|
+
<div class="controls" bind:this={controlsRef}>
|
|
140
|
+
<button type="button" on:click={handleClear} class="clear" class:active={query.length > 0}>
|
|
141
|
+
<Circle />
|
|
142
|
+
</button>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<style lang="scss">
|
|
147
|
+
@import '../styles/scss/base.scss';
|
|
148
|
+
|
|
149
|
+
.autocomplete {
|
|
150
|
+
position: relative;
|
|
151
|
+
display: block;
|
|
152
|
+
color: white;
|
|
153
|
+
|
|
154
|
+
input {
|
|
155
|
+
@extend %copy;
|
|
156
|
+
border: 1px solid black;
|
|
157
|
+
border-radius: $border-radius-input;
|
|
158
|
+
background: transparent;
|
|
159
|
+
padding: 0 0.5em;
|
|
160
|
+
height: $input-height;
|
|
161
|
+
margin: 0;
|
|
162
|
+
display: block;
|
|
163
|
+
width: 100%;
|
|
164
|
+
&:focus-visible {
|
|
165
|
+
border-bottom-right-radius: 0;
|
|
166
|
+
border-bottom-left-radius: 0;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
input[type='text'] {
|
|
170
|
+
text-size-adjust: none;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
ul {
|
|
174
|
+
position: absolute;
|
|
175
|
+
top: 100%;
|
|
176
|
+
border: 1px solid black;
|
|
177
|
+
border-bottom-left-radius: $border-radius-input;
|
|
178
|
+
border-bottom-right-radius: $border-radius-input;
|
|
179
|
+
border-top: 0;
|
|
180
|
+
left: 0;
|
|
181
|
+
right: 0;
|
|
182
|
+
max-height: 10rem;
|
|
183
|
+
overflow-y: scroll;
|
|
184
|
+
z-index: 1000;
|
|
185
|
+
opacity: 0;
|
|
186
|
+
transition: 100ms;
|
|
187
|
+
pointer-events: none;
|
|
188
|
+
&:empty {
|
|
189
|
+
box-shadow: none;
|
|
190
|
+
}
|
|
191
|
+
&.active {
|
|
192
|
+
opacity: 1;
|
|
193
|
+
pointer-events: all;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.item {
|
|
197
|
+
@extend %caption;
|
|
198
|
+
padding: 0.5em;
|
|
199
|
+
width: 100%;
|
|
200
|
+
background: transparent;
|
|
201
|
+
border: 0;
|
|
202
|
+
text-align: left;
|
|
203
|
+
border-bottom: 1px solid rgba(black, 0.5);
|
|
204
|
+
cursor: pointer;
|
|
205
|
+
&.active, &:hover {
|
|
206
|
+
text-decoration: underline;
|
|
207
|
+
background-color: rgba(black, .1);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.clear {
|
|
213
|
+
position: absolute;
|
|
214
|
+
transform: translateY(-50%);
|
|
215
|
+
top: 50%;
|
|
216
|
+
right: 0.75rem;
|
|
217
|
+
width: 1.2rem;
|
|
218
|
+
height: 1.2rem;
|
|
219
|
+
background: transparent;
|
|
220
|
+
border: 0;
|
|
221
|
+
display: none;
|
|
222
|
+
&.active {
|
|
223
|
+
display: block;
|
|
224
|
+
}
|
|
225
|
+
&:hover,
|
|
226
|
+
&:focus {
|
|
227
|
+
color: $orange--1;
|
|
228
|
+
cursor: pointer;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
</style>
|
package/src/Intro.mdx
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { userEvent, within, expect } from "@storybook/test"
|
|
2
|
+
import Switcher from "."
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
title: "Example/Switcher",
|
|
6
|
+
tags: ["autodocs"],
|
|
7
|
+
component: Switcher,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const TwoOptions = {
|
|
11
|
+
args: {
|
|
12
|
+
options: ["Option A", "Option B"],
|
|
13
|
+
groupName: "two-options",
|
|
14
|
+
value: "Option A",
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const FourOptions = {
|
|
19
|
+
args: {
|
|
20
|
+
options: ["Apples", "Oranges", "Bananas", "Peaches"],
|
|
21
|
+
groupName: "four-options",
|
|
22
|
+
value: "Oranges",
|
|
23
|
+
},
|
|
24
|
+
play: async ({ canvasElement, step }) => {
|
|
25
|
+
const canvas = within(canvasElement)
|
|
26
|
+
|
|
27
|
+
await step("Clicking selects the expected option", async () => {
|
|
28
|
+
const optionA = canvas.getByLabelText("Apples")
|
|
29
|
+
const optionB = canvas.getByLabelText("Bananas")
|
|
30
|
+
await userEvent.click(optionA)
|
|
31
|
+
expect(optionA).toBeChecked()
|
|
32
|
+
expect(optionB).not.toBeChecked()
|
|
33
|
+
await userEvent.click(optionB)
|
|
34
|
+
expect(optionB).toBeChecked()
|
|
35
|
+
expect(optionA).not.toBeChecked()
|
|
36
|
+
})
|
|
37
|
+
},
|
|
38
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
|
|
3
|
+
// The options available in the switcher.
|
|
4
|
+
export let options: string[] = [];
|
|
5
|
+
|
|
6
|
+
// Machine-readable name for the form field. Should be unique to other fields in the form.
|
|
7
|
+
export let groupName: string = "";
|
|
8
|
+
|
|
9
|
+
// The currently selected option
|
|
10
|
+
export let value : string = options[0];
|
|
11
|
+
|
|
12
|
+
function optionToID(o:string) {
|
|
13
|
+
// TODO: This should use $id() when it comes out, so
|
|
14
|
+
// input IDs are guaranteed unique across the app
|
|
15
|
+
// See: https://github.com/sveltejs/svelte/issues/13108
|
|
16
|
+
return `${groupName}-option-${o.replace(/ /g, '-').toLowerCase()}`;
|
|
17
|
+
}
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<!--
|
|
21
|
+
Radio-like form component to choose exactly one of a given set of options.
|
|
22
|
+
@component
|
|
23
|
+
-->
|
|
24
|
+
|
|
25
|
+
<ol class="container">
|
|
26
|
+
{#each options as o (o)}
|
|
27
|
+
<li class:is-selected={o === value}>
|
|
28
|
+
<label for={optionToID(o)}>
|
|
29
|
+
{o}
|
|
30
|
+
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
31
|
+
<path
|
|
32
|
+
d="M19.7054 6.29119C20.0969 6.68077 20.0984 7.31393 19.7088 7.7054L9.75697 17.7054C9.56928 17.894 9.31416 18 9.04809 18C8.78201 18 8.52691 17.8939 8.33925 17.7053L4.2911 13.6365C3.90157 13.245 3.90318 12.6118 4.2947 12.2223C4.68621 11.8327 5.31938 11.8344 5.7089 12.2259L9.04825 15.5823L18.2912 6.2946C18.6808 5.90314 19.3139 5.90161 19.7054 6.29119Z"
|
|
33
|
+
fill="currentColor"
|
|
34
|
+
/>
|
|
35
|
+
</svg>
|
|
36
|
+
</label>
|
|
37
|
+
<input id={optionToID(o)} name={groupName} value={o} type="radio" bind:group={value} />
|
|
38
|
+
</li>
|
|
39
|
+
{/each}
|
|
40
|
+
</ol>
|
|
41
|
+
|
|
42
|
+
<style lang="scss">
|
|
43
|
+
@import "../styles/scss/base.scss";
|
|
44
|
+
|
|
45
|
+
.container {
|
|
46
|
+
width: 100%;
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-direction: row;
|
|
49
|
+
border-radius: $border-radius-input;
|
|
50
|
+
overflow: hidden;
|
|
51
|
+
padding: 0;
|
|
52
|
+
margin: 0;
|
|
53
|
+
overflow: hidden;
|
|
54
|
+
border: 1px solid black;
|
|
55
|
+
outline-offset: 2px;
|
|
56
|
+
|
|
57
|
+
&:focus-within,
|
|
58
|
+
&:active {
|
|
59
|
+
outline: 2px solid lightgray;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
li {
|
|
63
|
+
display: contents;
|
|
64
|
+
&:last-child label {
|
|
65
|
+
border-right: 0;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
input {
|
|
69
|
+
position: absolute;
|
|
70
|
+
left: -999px;
|
|
71
|
+
}
|
|
72
|
+
label {
|
|
73
|
+
@extend %copy;
|
|
74
|
+
line-height: 1;
|
|
75
|
+
padding-top: 0.1em;
|
|
76
|
+
height: $input-height;
|
|
77
|
+
cursor: pointer;
|
|
78
|
+
margin: 0;
|
|
79
|
+
flex-basis: 0;
|
|
80
|
+
flex-grow: 1;
|
|
81
|
+
align-items: center;
|
|
82
|
+
display: flex;
|
|
83
|
+
justify-content: center;
|
|
84
|
+
color: black;
|
|
85
|
+
position: relative;
|
|
86
|
+
transition: $transition-fast;
|
|
87
|
+
text-underline-offset: 0.1em;
|
|
88
|
+
border-right: 1px solid black;
|
|
89
|
+
&:hover,
|
|
90
|
+
&:focus-visible {
|
|
91
|
+
text-decoration: underline;
|
|
92
|
+
}
|
|
93
|
+
svg {
|
|
94
|
+
position: absolute;
|
|
95
|
+
left: 0.65em;
|
|
96
|
+
width: 1em;
|
|
97
|
+
height: auto;
|
|
98
|
+
opacity: 0;
|
|
99
|
+
transition: $transition-fast;
|
|
100
|
+
display: block;
|
|
101
|
+
}
|
|
102
|
+
.is-selected & {
|
|
103
|
+
background: black;
|
|
104
|
+
color: white;
|
|
105
|
+
svg {
|
|
106
|
+
opacity: 1;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
</style>
|
package/src/app.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// See https://kit.svelte.dev/docs/types#app
|
|
2
|
+
// for information about these interfaces
|
|
3
|
+
declare global {
|
|
4
|
+
namespace App {
|
|
5
|
+
// interface Error {}
|
|
6
|
+
// interface Locals {}
|
|
7
|
+
// interface PageData {}
|
|
8
|
+
// interface PageState {}
|
|
9
|
+
// interface Platform {}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { };
|
package/src/app.html
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
%sveltekit.head%
|
|
8
|
+
</head>
|
|
9
|
+
<body data-sveltekit-preload-data="hover">
|
|
10
|
+
<div style="display: contents">%sveltekit.body%</div>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="123" height="118" viewBox="0 0 123 118" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M61.5 118C95.4655 118 123 91.5848 123 59C123 26.4152 95.4655 0 61.5 0C27.5345 0 0 26.4152 0 59C0 91.5848 27.5345 118 61.5 118ZM81.9362 27.0104C85.2557 23.6909 90.6376 23.691 93.957 27.0104C97.2765 30.3299 97.2765 35.7118 93.957 39.0312L73.5045 59.4837L93.957 79.9363C97.2765 83.2557 97.2765 88.6376 93.957 91.9571C90.6376 95.2765 85.2556 95.2765 81.9362 91.9571L61.4837 71.5045L42.0312 90.9571C38.7117 94.2765 33.3298 94.2765 30.0103 90.9571C26.6909 87.6376 26.6909 82.2557 30.0103 78.9362L49.4629 59.4837L30.0103 40.0312C26.6909 36.7118 26.6909 31.3299 30.0103 28.0104C33.3298 24.6909 38.7117 24.6909 42.0312 28.0104L61.4837 47.4629L81.9362 27.0104Z" fill="#C4C4C4"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> <!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
|
2
|
+
<path d="M344 0H488c13.3 0 24 10.7 24 24V168c0 9.7-5.8 18.5-14.8 22.2s-19.3 1.7-26.2-5.2l-39-39-87 87c-9.4 9.4-24.6 9.4-33.9 0l-32-32c-9.4-9.4-9.4-24.6 0-33.9l87-87L327 41c-6.9-6.9-8.9-17.2-5.2-26.2S334.3 0 344 0zM184 496H40c-13.3 0-24-10.7-24-24V328c0-9.7 5.8-18.5 14.8-22.2s19.3-1.7 26.2 5.2l39 39 87-87c9.4-9.4 24.6-9.4 33.9 0l32 32c9.4 9.4 9.4 24.6 0 33.9l-87 87 39 39c6.9 6.9 8.9 17.2 5.2 26.2s-12.5 14.8-22.2 14.8z"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"> <!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
|
2
|
+
<path d="M128 40c0-22.1 17.9-40 40-40s40 17.9 40 40V188.2c8.5-7.6 19.7-12.2 32-12.2c25.3 0 46 19.5 47.9 44.3c8.5-7.7 19.8-12.3 32.1-12.3c25.3 0 46 19.5 47.9 44.3c8.5-7.7 19.8-12.3 32.1-12.3c26.5 0 48 21.5 48 48v32 64c0 70.7-57.3 128-128 128l-16 0H240l-.1 0h-5.2c-5 0-9.9-.3-14.7-1c-55.3-5.6-106.2-34-140-79L8 336c-13.3-17.7-9.7-42.7 8-56s42.7-9.7 56 8l56 74.7V40zM240 304c0-8.8-7.2-16-16-16s-16 7.2-16 16v96c0 8.8 7.2 16 16 16s16-7.2 16-16V304zm48-16c-8.8 0-16 7.2-16 16v96c0 8.8 7.2 16 16 16s16-7.2 16-16V304c0-8.8-7.2-16-16-16zm80 16c0-8.8-7.2-16-16-16s-16 7.2-16 16v96c0 8.8 7.2 16 16 16s16-7.2 16-16V304z"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> <!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
|
2
|
+
<path d="M473 7c-9.4-9.4-24.6-9.4-33.9 0l-87 87L313 55c-6.9-6.9-17.2-8.9-26.2-5.2S272 62.3 272 72V216c0 13.3 10.7 24 24 24H440c9.7 0 18.5-5.8 22.2-14.8s1.7-19.3-5.2-26.2l-39-39 87-87c9.4-9.4 9.4-24.6 0-33.9L473 7zM216 272H72c-9.7 0-18.5 5.8-22.2 14.8s-1.7 19.3 5.2 26.2l39 39L7 439c-9.4 9.4-9.4 24.6 0 33.9l32 32c9.4 9.4 24.6 9.4 33.9 0l87-87 39 39c6.9 6.9 17.2 8.9 26.2 5.2s14.8-12.5 14.8-22.2V296c0-13.3-10.7-24-24-24z"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 55.99 55.7"
|
|
2
|
+
><defs
|
|
3
|
+
><style>
|
|
4
|
+
.b,
|
|
5
|
+
.c {
|
|
6
|
+
stroke-miterlimit: 10;
|
|
7
|
+
}
|
|
8
|
+
.c {
|
|
9
|
+
stroke-width: 2px;
|
|
10
|
+
}
|
|
11
|
+
</style></defs
|
|
12
|
+
><path
|
|
13
|
+
class="b"
|
|
14
|
+
d="M35.54,14.39c-.61,.58-.91,1.4-.91,2.22l-.3-.35c-1.62-2.33-3.75-2.8-5.37-1.17-.71,.7-1.12,1.52-1.32,2.22-1.01-.82-2.54-1.17-3.95,.12-.81,.7-1.22,1.52-1.42,2.33-1.93-2.68-3.75-5.37-3.95-5.48-.61-.93-2.94-1.87-4.77-.23-1.52,1.4-1.22,3.85-.81,5.13,0,.12,.1,.23,.1,.23l8.72,13.3c-1.62-.82-3.55-1.28-4.56-1.05-1.12,.23-2.33,1.87-2.54,3.5-.2,1.52,.41,2.8,1.62,3.62,1.01,.58,1.93,1.52,3.04,2.57,1.42,1.4,3.24,3.03,5.78,4.78,4.06,2.8,10.85,3.38,12.78,3.5l2.13,3.03c2.03,2.92,5.88,3.38,8.42,1.05l5.07-4.78c2.54-2.33,2.94-6.77,.91-9.69l-2.54-3.62c-.1-3.27-1.93-7.7-2.03-7.94,0-.12-.1-.12-.1-.23l-8.72-12.37c-1.32-1.98-3.45-2.33-5.27-.7Zm12.47,14.59c.51,1.4,1.83,5.02,1.83,7.24,0,.23,.1,.58,.2,.7l2.74,3.85c1.42,1.98,1.12,4.78-.61,6.42l-5.07,4.78c-1.72,1.63-4.16,1.28-5.58-.7l-2.33-3.38c-.2-.23-.51-.47-.81-.47-.1,0-8.11-.35-12.27-3.15-2.43-1.63-4.06-3.27-5.48-4.67-1.22-1.17-2.23-2.1-3.35-2.8-.61-.35-.61-.82-.61-1.17,.1-.7,.51-1.28,.71-1.52q.1-.12,.2-.12c.71-.23,3.55,.7,4.97,1.98,.61,.47,1.01,1.05,1.52,1.52,.81,.93,1.42,1.63,2.33,1.63,.41,0,.71-.23,.91-.7,.2-.35,.1-.82-.1-1.17L14.65,18.12c-.1-.47-.41-1.75,.2-2.33,.91-.82,1.93-.12,1.83-.12,.3,.47,7.71,10.85,7.71,10.85,.3,.47,.91,.58,1.42,.23,.41-.35,.51-1.05,.3-1.52l-2.03-3.73c0-.35,0-1.4,.91-2.22,.81-.7,1.52-.12,1.83,.23l3.04,3.27c.41,.47,1.01,.35,1.42,0,.41-.47,.41-1.17,.1-1.63l-1.72-2.33c-.1-.35-.2-1.05,.71-1.87,1.01-.93,1.93-.12,2.54,.82,.81,1.05,3.24,3.5,3.45,3.73,.41,.35,.91,.35,1.32,0,.41-.35,.51-1.05,.2-1.52-.81-1.4-1.42-3.38-.91-3.73,1.12-1.05,2.03-.35,2.54,.35l8.52,12.37h0Z"
|
|
15
|
+
/><path
|
|
16
|
+
class="c"
|
|
17
|
+
d="M21.36,13.18c.28,.8,1.12,1.12,1.83,.8l3.79-2.08c.14,0,.14-.16,.28-.16,.56-.48,.7-1.28,.42-1.92-.28-.8-1.12-1.12-1.83-.8l-3.79,1.92c-.7,.48-.98,1.44-.7,2.24Z"
|
|
18
|
+
/><path
|
|
19
|
+
class="c"
|
|
20
|
+
d="M10.69,10.46c.56-.48,.7-1.6,.28-2.24l-2.67-3.84c-.42-.64-1.4-.8-1.97-.32-.7,.64-.7,1.76-.28,2.4l2.67,3.68c.56,.64,1.4,.8,1.97,.32Z"
|
|
21
|
+
/><path
|
|
22
|
+
class="c"
|
|
23
|
+
d="M17.85,8.54c.28-.16,.42-.48,.56-.96l.84-4.64c.14-.8-.28-1.76-1.12-1.92-.7-.16-1.54,.32-1.68,1.28l-.84,4.64c-.14,.8,.28,1.76,1.12,1.92,.42,.16,.84,0,1.12-.32Z"
|
|
24
|
+
/><path
|
|
25
|
+
class="c"
|
|
26
|
+
d="M6.62,18.3c.28,0,.7-.16,.84-.32,.28-.32,.56-.8,.56-1.28,0-.96-.56-1.6-1.4-1.6H2.4c-.84,0-1.4,.64-1.4,1.6s.56,1.6,1.4,1.6H6.62Z"
|
|
27
|
+
/><path
|
|
28
|
+
class="c"
|
|
29
|
+
d="M8.16,29.17c.14,0,.14-.16,.28-.32l2.67-3.84c.42-.64,.42-1.76-.28-2.24-.56-.48-1.54-.48-1.97,.32l-2.67,3.84c-.42,.64-.42,1.76,.28,2.24,.42,.48,1.12,.48,1.68,0Z"
|
|
30
|
+
/></svg
|
|
31
|
+
>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="times-circle" class="svg-inline--fa fa-times-circle fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm121.6 313.1c4.7 4.7 4.7 12.3 0 17L338 377.6c-4.7 4.7-12.3 4.7-17 0L256 312l-65.1 65.6c-4.7 4.7-12.3 4.7-17 0L134.4 338c-4.7-4.7-4.7-12.3 0-17l65.6-65-65.6-65.1c-4.7-4.7-4.7-12.3 0-17l39.6-39.6c4.7-4.7 12.3-4.7 17 0l65 65.7 65.1-65.6c4.7-4.7 12.3-4.7 17 0l39.6 39.6c4.7 4.7 4.7 12.3 0 17L312 256l65.6 65.1z"></path></svg>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dispatch a click event outside of the given element.
|
|
3
|
+
* @param {Node} node
|
|
4
|
+
*/
|
|
5
|
+
export function clickOutside(node) {
|
|
6
|
+
const handleClick = (event) => {
|
|
7
|
+
if (node && !node.contains(event.target) && !event.defaultPrevented) {
|
|
8
|
+
node.dispatchEvent(
|
|
9
|
+
new CustomEvent('clickOutside', {
|
|
10
|
+
detail: { node, event }
|
|
11
|
+
})
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
document.addEventListener('click', handleClick, true);
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
destroy() {
|
|
20
|
+
document.removeEventListener('click', handleClick, true);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
body {
|
|
2
|
+
--petrol--1: #00677f;
|
|
3
|
+
--eisblau--1: #00dbd7;
|
|
4
|
+
--türkisblau--1: #007e8f;
|
|
5
|
+
--dunkelblau--1: #0a1e3c;
|
|
6
|
+
--orange--1: #ff4e3c;
|
|
7
|
+
--purpur--1: #5c267e;
|
|
8
|
+
--rosarot--1: #ff003c;
|
|
9
|
+
--pink--1: #ff0096;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
body[data-stationid="swraktuell"] {
|
|
13
|
+
--swrdata-color-primary: var(--petrol--1);
|
|
14
|
+
--swrdata-color-highlight: var(--orange--1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
body[data-stationid="swr3"] {
|
|
18
|
+
--swrdata-color-primary: var(--rosarot--1);
|
|
19
|
+
--swrdata-color-highlight: var(--pink--1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* load fonts for swr3 */
|
|
23
|
+
|
|
24
|
+
@font-face {
|
|
25
|
+
font-family: TheMix;
|
|
26
|
+
src:
|
|
27
|
+
url(https://www.swr.de/assets/fonts/transfonter/TheMixC5-3_Light.woff2) format("woff2"),
|
|
28
|
+
url(https://www.swr.de/assets/fonts/transfonter/TheMixC5-3_Light.woff) format("woff");
|
|
29
|
+
font-weight: 300;
|
|
30
|
+
font-display: swap;
|
|
31
|
+
font-style: normal;
|
|
32
|
+
}
|
|
33
|
+
@font-face {
|
|
34
|
+
font-family: TheMix;
|
|
35
|
+
src:
|
|
36
|
+
url(https://www.swr.de/assets/fonts/transfonter/TheMixC5-4_SemiLight.woff2) format("woff2"),
|
|
37
|
+
url(https://www.swr.de/assets/fonts/transfonter/TheMixC5-4_SemiLight.woff) format("woff");
|
|
38
|
+
font-weight: 400;
|
|
39
|
+
font-display: swap;
|
|
40
|
+
font-style: normal;
|
|
41
|
+
}
|
|
42
|
+
@font-face {
|
|
43
|
+
font-family: TheMix;
|
|
44
|
+
src:
|
|
45
|
+
url(https://www.swr.de/assets/fonts/transfonter/TheMixC5-5_Plain.woff2) format("woff2"),
|
|
46
|
+
url(https://www.swr.de/assets/fonts/transfonter/TheMixC5-5_Plain.woff) format("woff");
|
|
47
|
+
font-weight: 500;
|
|
48
|
+
font-display: swap;
|
|
49
|
+
font-style: normal;
|
|
50
|
+
}
|
|
51
|
+
@font-face {
|
|
52
|
+
font-family: TheMix;
|
|
53
|
+
src:
|
|
54
|
+
url(https://www.swr.de/assets/fonts/transfonter/TheMixC5-6_SemiBold.woff2) format("woff2"),
|
|
55
|
+
url(https://www.swr.de/assets/fonts/transfonter/TheMixC5-6_SemiBold.woff) format("woff");
|
|
56
|
+
font-weight: 600;
|
|
57
|
+
font-display: swap;
|
|
58
|
+
font-style: normal;
|
|
59
|
+
}
|
|
60
|
+
@font-face {
|
|
61
|
+
font-family: TheMix;
|
|
62
|
+
src:
|
|
63
|
+
url(https://www.swr.de/assets/fonts/transfonter/TheMixC5-7_Bold.woff2) format("woff2"),
|
|
64
|
+
url(https://www.swr.de/assets/fonts/transfonter/TheMixC5-7_Bold.woff) format("woff");
|
|
65
|
+
font-weight: 700;
|
|
66
|
+
font-display: swap;
|
|
67
|
+
font-style: normal;
|
|
68
|
+
}
|
|
69
|
+
@font-face {
|
|
70
|
+
font-family: TheMix;
|
|
71
|
+
src:
|
|
72
|
+
url(https://www.swr.de/assets/fonts/transfonter/TheMixC5-8_ExtraBold.woff2) format("woff2"),
|
|
73
|
+
url(https://www.swr.de/assets/fonts/transfonter/TheMixC5-8_ExtraBold.woff) format("woff");
|
|
74
|
+
font-weight: 800;
|
|
75
|
+
font-display: swap;
|
|
76
|
+
font-style: normal;
|
|
77
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// swr main colors
|
|
2
|
+
$petrol--1: #05556a;
|
|
3
|
+
$eisblau--1: #00dbd7;
|
|
4
|
+
$dunkelblau--1: #0a1e3c;
|
|
5
|
+
$orange--1: #ff4e3c;
|
|
6
|
+
$purpur--1: #5c267e;
|
|
7
|
+
$rosarot--1: #ff003c;
|
|
8
|
+
|
|
9
|
+
// impfungen
|
|
10
|
+
$erst: #929292;
|
|
11
|
+
$voll: rgb(0, 150, 160);
|
|
12
|
+
$boost: #05556a;
|
|
13
|
+
|
|
14
|
+
// min max
|
|
15
|
+
$inzidenz--max: #05556a;
|
|
16
|
+
$inzidenz--min: transparentize($inzidenz--max, 0.1);
|
|
17
|
+
|
|
18
|
+
// scale with 10 options
|
|
19
|
+
$inzidenz--10: rgb(4, 52, 58);
|
|
20
|
+
$inzidenz--9: rgb(5, 70, 84);
|
|
21
|
+
$inzidenz--8: rgb(5, 92, 112);
|
|
22
|
+
$inzidenz--7: rgb(4, 128, 142);
|
|
23
|
+
$inzidenz--6: rgb(1, 165, 172);
|
|
24
|
+
$inzidenz--5: rgb(1, 203, 203);
|
|
25
|
+
$inzidenz--4: rgb(103, 229, 228);
|
|
26
|
+
$inzidenz--3: rgb(177, 245, 250);
|
|
27
|
+
$inzidenz--2: rgb(203, 245, 250);
|
|
28
|
+
$inzidenz--1: rgb(218, 241, 244);
|
|
29
|
+
|
|
30
|
+
$inzidenz-1--8: rgb(4, 52, 58);
|
|
31
|
+
$inzidenz-1--7: rgb(5, 69, 82);
|
|
32
|
+
$inzidenz-1--6: rgb(5, 86, 107);
|
|
33
|
+
$inzidenz-1--5: rgb(2, 122, 137);
|
|
34
|
+
$inzidenz-1--4: rgb(0, 150, 160);
|
|
35
|
+
$inzidenz-1--3: rgb(0, 218, 214);
|
|
36
|
+
$inzidenz-1--2: rgb(194, 247, 254);
|
|
37
|
+
$inzidenz-1--1: rgb(218, 241, 244);
|
|
38
|
+
|
|
39
|
+
// Spritpreise
|
|
40
|
+
$diesel-min: #f9ebb7;
|
|
41
|
+
$diesel-max: #b7950d;
|
|
42
|
+
$e5-min: #d0f0f1;
|
|
43
|
+
$e5-max: #05556a;
|
|
44
|
+
$e10-min: #ebd9e1;
|
|
45
|
+
$e10-max: #7e2d41;
|
|
46
|
+
|
|
47
|
+
// extra colors
|
|
48
|
+
$hellgrau--1: #b8b8b8;
|
|
49
|
+
$hellgrau--2: #929292;
|
|
50
|
+
$hellgrau--3: #7b7b7b;
|
|
51
|
+
$hellgrau--4: #494949;
|
|
52
|
+
$schwarzgrau--1: #383838;
|
|
53
|
+
$schwarzgrau--4: #161616;
|
|
54
|
+
|
|
55
|
+
$schatten-grau: #eeeeee;
|
|
56
|
+
$hintergrund-grau: #f5f5f5;
|
|
57
|
+
|
|
58
|
+
body {
|
|
59
|
+
--petrol--1: #00677f;
|
|
60
|
+
--eisblau--1: #00dbd7;
|
|
61
|
+
--türkisblau--1: #007e8f;
|
|
62
|
+
--dunkelblau--1: #0a1e3c;
|
|
63
|
+
--orange--1: #ff4e3c;
|
|
64
|
+
--purpur--1: #5c267e;
|
|
65
|
+
|
|
66
|
+
--rosarot--1: #ff003c;
|
|
67
|
+
--pink--1: #ff0096;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
body[stationid='swraktuell'] {
|
|
71
|
+
--swrdata-color-primary: var(--petrol--1);
|
|
72
|
+
--swrdata-color-highlight: var(--orange--1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
body[stationid='swr3'] {
|
|
76
|
+
--swrdata-color-primary: var(--rosarot--1);
|
|
77
|
+
--swrdata-color-highlight: var;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
:export {
|
|
81
|
+
dieselMin: $diesel-min;
|
|
82
|
+
dieselMax: $diesel-max;
|
|
83
|
+
e5Min: $e5-min;
|
|
84
|
+
e5Max: $e5-max;
|
|
85
|
+
e10Min: $e10-min;
|
|
86
|
+
e10Max: $e10-max;
|
|
87
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
$swr-inner-container-width-m: 490px;
|
|
2
|
+
$swr-inner-container-width-l: 730px;
|
|
3
|
+
$swr-inner-container-width-xl: 976px;
|
|
4
|
+
$swr-padding-removal: 20px;
|
|
5
|
+
$reanimation-width: 1440px;
|
|
6
|
+
$reanimation-max-width: 1440px;
|
|
7
|
+
$reanimation-max-text-width: 864px;
|
|
8
|
+
$reanimation-ff-text: "SWR-VAR-Text";
|
|
9
|
+
$reanimation-ff-sans: "SWR-VAR-Sans";
|
|
10
|
+
$reanimation-ff-slab: "SWR-VAR-Slab";
|
|
11
|
+
$transition-fast: 150ms;
|
|
12
|
+
|
|
13
|
+
// Padding
|
|
14
|
+
$reanimation-padding-s: 16px;
|
|
15
|
+
$reanimation-padding-m: 32px;
|
|
16
|
+
$reanimation-padding-l: 48px;
|
|
17
|
+
$reanimation-padding-xl: 64px;
|
|
18
|
+
$reanimation-padding-xxl: 96px;
|
|
19
|
+
$reanimation-padding-xxxl: 112px;
|
|
20
|
+
|
|
21
|
+
// Breakpoints
|
|
22
|
+
$break-phone: 510px;
|
|
23
|
+
$break-tablet: 992px;
|
|
24
|
+
$break-desktop-small: 1200px;
|
|
25
|
+
$break-desktop-large: 1400px;
|
|
26
|
+
$break-desktop-xl: 1400px;
|
|
27
|
+
|
|
28
|
+
// Colors
|
|
29
|
+
$reanimation-black: #0c0c0c;
|
|
30
|
+
$reanimation-violetblue: #1d0b40;
|
|
31
|
+
$reanimation-violetblue-hover: #2d155d;
|
|
32
|
+
$reanimation-violet: #5920c0;
|
|
33
|
+
$reanimation-blue: #3053d9;
|
|
34
|
+
$reanimation-warmgrey: #dbd5cd;
|
|
35
|
+
$reanimation-activeorange: #ff3300;
|
|
36
|
+
|
|
37
|
+
// Breakout radius
|
|
38
|
+
$border-radius-box: 8px;
|
|
39
|
+
$border-radius-input: 4px;
|
|
40
|
+
|
|
41
|
+
// max container width
|
|
42
|
+
$app-max-width: 650px !default;
|
|
43
|
+
$input-height: 2.35em;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
@mixin flex-center {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
justify-content: center;
|
|
5
|
+
align-items: center;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
@mixin respond-min($width) {
|
|
9
|
+
@media screen and (min-width: $width) {
|
|
10
|
+
@content;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@mixin respond-max($width) {
|
|
15
|
+
@media screen and (max-width: $width) {
|
|
16
|
+
@content;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@mixin hover {
|
|
21
|
+
@media (hover: hover) {
|
|
22
|
+
&:hover {
|
|
23
|
+
@content;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
&:active {
|
|
27
|
+
// &:focus {
|
|
28
|
+
@content;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
@use 'sass:math';
|
|
2
|
+
|
|
3
|
+
@font-face {
|
|
4
|
+
font-family: 'SWR-VAR-Sans';
|
|
5
|
+
font-weight: 400 600 700;
|
|
6
|
+
font-display: swap;
|
|
7
|
+
src:
|
|
8
|
+
local('SWR-VAR-Sans'),
|
|
9
|
+
url('https://www.swr.de/assets/fonts/swr_type/SWR_VAR_WEB/SWR-VAR-Sans.woff2') format('woff2');
|
|
10
|
+
}
|
|
11
|
+
@font-face {
|
|
12
|
+
font-family: 'SWR-VAR-Text';
|
|
13
|
+
font-weight: 400 600 700;
|
|
14
|
+
font-display: swap;
|
|
15
|
+
src:
|
|
16
|
+
local('SWR-VAR-Text'),
|
|
17
|
+
url('https://www.swr.de/assets/fonts/swr_type/SWR_VAR_WEB/SWR-VAR-Text.woff2') format('woff2');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
$sans: 'SWR-VAR-Sans', sans-serif;
|
|
21
|
+
$text: 'SWR-VAR-Text', sans-serif;
|
|
22
|
+
|
|
23
|
+
%caption {
|
|
24
|
+
font-size: 0.9rem;
|
|
25
|
+
line-height: 1;
|
|
26
|
+
letter-spacing: 0.0045em;
|
|
27
|
+
}
|
|
28
|
+
%caption-bold {
|
|
29
|
+
@extend %caption;
|
|
30
|
+
font-weight: 700;
|
|
31
|
+
}
|
|
32
|
+
%copy {
|
|
33
|
+
font-family: $text;
|
|
34
|
+
font-size: 1.1rem;
|
|
35
|
+
line-height: 1.45;
|
|
36
|
+
letter-spacing: 0.0045em;
|
|
37
|
+
}
|
|
38
|
+
%copy-bold {
|
|
39
|
+
@extend %copy;
|
|
40
|
+
font-weight: 700;
|
|
41
|
+
}
|
|
42
|
+
%h3 {
|
|
43
|
+
@extend %copy;
|
|
44
|
+
font-family: $sans;
|
|
45
|
+
font-size: 2.2rem;
|
|
46
|
+
line-height: 1.15;
|
|
47
|
+
font-weight: 600;
|
|
48
|
+
letter-spacing: 0.0025em;
|
|
49
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { timeFormat } from "d3-time-format"
|
|
2
|
+
|
|
3
|
+
const formats = {
|
|
4
|
+
dayMonthYear: "%-d.%-m.%Y",
|
|
5
|
+
dayMonth: "%-d.%-m.",
|
|
6
|
+
dayMonthYearShort: "%-d.%-m.%y",
|
|
7
|
+
dayMonthHourMinute: "%-d.%-m., %-H.%M Uhr",
|
|
8
|
+
dayMonthYearHourMinute: "%-d.%-m.%Y, %-H.%M Uhr",
|
|
9
|
+
hourMinuteSophora: "%-H:%M Uhr",
|
|
10
|
+
hourMinute: "%-H.%M",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const formatDate = ({ date, format = "dayMonthYear" }) => {
|
|
14
|
+
return timeFormat(formats[format] || format)(new Date(date))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default formatDate
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { formatLocale } from 'd3-format';
|
|
2
|
+
|
|
3
|
+
export const defaultFormat = ',.1f';
|
|
4
|
+
export const defaultLocale = {
|
|
5
|
+
decimal: ',',
|
|
6
|
+
thousands: ' ',
|
|
7
|
+
grouping: [3],
|
|
8
|
+
currency: ['', '€']
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default formatNumber = ({ number, format = defaultFormat, locale = defaultLocale }) => {
|
|
12
|
+
return formatLocale(locale).format(format)(number);
|
|
13
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import Color from 'color';
|
|
2
|
+
import { range } from 'd3-array';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get color range of n colors
|
|
6
|
+
* @param {*} from
|
|
7
|
+
* @param {*} to
|
|
8
|
+
* @param {*} props
|
|
9
|
+
* @returns
|
|
10
|
+
*/
|
|
11
|
+
const getColorsBetween = (from, to, props = { n: 1, includeFromTo: false }) => {
|
|
12
|
+
const step = 1 / (props.n + 1);
|
|
13
|
+
const fromColor = Color(from);
|
|
14
|
+
const toColor = Color(to);
|
|
15
|
+
const colorsBetween = range(1, props.n + 1)
|
|
16
|
+
.map((i) => fromColor.mix(toColor, i * step))
|
|
17
|
+
.map((c) => c.hex().toString());
|
|
18
|
+
if (props.includeFromTo) {
|
|
19
|
+
return [from, ...colorsBetween, to];
|
|
20
|
+
} else {
|
|
21
|
+
return colorsBetween;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default getColorsBetween;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get current url params or - if on production - params from embed url.
|
|
3
|
+
* @param {object} embed root dom element
|
|
4
|
+
* @returns {object} url params
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const getEmbedContext = (target) => {
|
|
8
|
+
let url;
|
|
9
|
+
if (
|
|
10
|
+
import.meta.env.DEV ||
|
|
11
|
+
window.location.origin === 'https://d.swr.de' ||
|
|
12
|
+
window.location.origin === 'https://static.datenhub.net' ||
|
|
13
|
+
window.location.href.includes('apidata.googleusercontent.com') ||
|
|
14
|
+
window.location.href.includes('storage.googleapis.com')
|
|
15
|
+
) {
|
|
16
|
+
url = window.location.href;
|
|
17
|
+
} else {
|
|
18
|
+
const parent = target.parentNode || target.parentNode.parentNode;
|
|
19
|
+
url = parent.dataset.url;
|
|
20
|
+
}
|
|
21
|
+
const data = {};
|
|
22
|
+
for (const [key, value] of new URL(url).searchParams) {
|
|
23
|
+
data[key] = value;
|
|
24
|
+
}
|
|
25
|
+
return data;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default getEmbedContext;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { merge } from 'topojson-client';
|
|
2
|
+
import { range } from 'd3-array';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Calc laender borders from kreis or gemeinden topojson
|
|
6
|
+
* @param {object} topojson
|
|
7
|
+
* @returns geojson
|
|
8
|
+
*/
|
|
9
|
+
const getLaenderFromTopo = (topo) => {
|
|
10
|
+
const states = [];
|
|
11
|
+
const stateIds = range(1, 17).map((n) => String(n).padStart(2, '0'));
|
|
12
|
+
const key = Object.keys(topo.objects)[0];
|
|
13
|
+
|
|
14
|
+
stateIds.forEach((id) => {
|
|
15
|
+
const state = {
|
|
16
|
+
geometry: merge(
|
|
17
|
+
topo,
|
|
18
|
+
topo.objects[key].geometries.filter(
|
|
19
|
+
(g) => g.properties.id && g.properties.id.startsWith(id)
|
|
20
|
+
)
|
|
21
|
+
),
|
|
22
|
+
id,
|
|
23
|
+
type: 'Feature',
|
|
24
|
+
properties: { id }
|
|
25
|
+
};
|
|
26
|
+
states.push(state);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
type: 'FeatureCollection',
|
|
31
|
+
features: states
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default getLaenderFromTopo;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const map = {
|
|
2
|
+
"01": "Schleswig-Holstein",
|
|
3
|
+
"02": "Hamburg",
|
|
4
|
+
"03": "Niedersachsen",
|
|
5
|
+
"04": "Bremen",
|
|
6
|
+
"05": "Nordrhein-Westfalen",
|
|
7
|
+
"06": "Hessen",
|
|
8
|
+
"07": "Rheinland-Pfalz",
|
|
9
|
+
"08": "Baden-Württemberg",
|
|
10
|
+
"09": "Bayern",
|
|
11
|
+
10: "Saarland",
|
|
12
|
+
11: "Berlin",
|
|
13
|
+
12: "Brandenburg",
|
|
14
|
+
13: "Mecklenburg-Vorpommern",
|
|
15
|
+
14: "Sachsen",
|
|
16
|
+
15: "Sachsen-Anhalt",
|
|
17
|
+
16: "Thüringen",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get nicename for land ags
|
|
22
|
+
* @param {string} ags
|
|
23
|
+
* @returns {string} nicename
|
|
24
|
+
*/
|
|
25
|
+
const getLaenderNicenameFromAgs = (ags) => map[ags]
|
|
26
|
+
|
|
27
|
+
export default getLaenderNicenameFromAgs
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { SvelteComponent } from 'svelte';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Check if given component is a svelte component.
|
|
5
|
+
* @param {*} component
|
|
6
|
+
* @returns {boolean}
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const isSvelteComponent = (component) => {
|
|
10
|
+
return SvelteComponent.isPrototypeOf(component) || typeof component === 'function';
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default isSvelteComponent;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const SELECTORS = {
|
|
2
|
+
showScrollUp: "a.back-to-top",
|
|
3
|
+
showPlayerbar: "#playerbar",
|
|
4
|
+
showSharing: ".sharing",
|
|
5
|
+
showAuthors: ".meta-top .meta-authors",
|
|
6
|
+
allowZoom: "meta[name=viewport]",
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const IS_MOBILE = window.innerWidth < 768
|
|
10
|
+
|
|
11
|
+
const prepareSophoraModel = (config) => {
|
|
12
|
+
console.log("###SWRDATA### updating dom with following config: ", config)
|
|
13
|
+
Object.entries(config).forEach(([k, v], _) => {
|
|
14
|
+
const element = document.querySelector(SELECTORS[k])
|
|
15
|
+
// show/hide elements
|
|
16
|
+
if (k.startsWith("show")) {
|
|
17
|
+
if ((v === "none" || (v === "mobile" && IS_MOBILE) || (v === "desktop" && !IS_MOBILE)) && element) {
|
|
18
|
+
element.style.display = "none"
|
|
19
|
+
element.style.visibility = "hidden"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// enable/disable zoom
|
|
23
|
+
if (k === "allowZoom") {
|
|
24
|
+
if (!v && element) {
|
|
25
|
+
element.setAttribute("content", "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0")
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default prepareSophoraModel
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const OFFSET = -50;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Scroll to top of given dom element with given offset.
|
|
5
|
+
* @param {object} ref to dom element
|
|
6
|
+
* @param {number} offset
|
|
7
|
+
*/
|
|
8
|
+
const scrollIntoViewWithOffset = (ref, offset) => {
|
|
9
|
+
const yOffset = offset !== 'undefined' ? offset : OFFSET;
|
|
10
|
+
const y = ref.getBoundingClientRect().top + window.pageYOffset + yOffset;
|
|
11
|
+
window.scrollTo({ top: y, behavior: 'smooth' });
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default scrollIntoViewWithOffset;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { feature } from 'topojson-client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Convert topojson to geojson
|
|
5
|
+
* @param {object} topojson
|
|
6
|
+
* @returns {object} geojson
|
|
7
|
+
*/
|
|
8
|
+
const topoToGeo = (topojson) => {
|
|
9
|
+
const key = Object.keys(topojson.objects)[0];
|
|
10
|
+
return feature(topojson, topojson.objects[key]);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default topoToGeo;
|
|
Binary file
|
package/svelte.config.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import adapter from '@sveltejs/adapter-auto';
|
|
2
|
+
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
|
3
|
+
|
|
4
|
+
/** @type {import('@sveltejs/kit').Config} */
|
|
5
|
+
const config = {
|
|
6
|
+
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
|
7
|
+
// for more information about preprocessors
|
|
8
|
+
preprocess: vitePreprocess(),
|
|
9
|
+
|
|
10
|
+
kit: {
|
|
11
|
+
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
|
12
|
+
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
|
13
|
+
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
|
14
|
+
adapter: adapter()
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default config;
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./.svelte-kit/tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"allowJs": true,
|
|
5
|
+
"checkJs": true,
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"forceConsistentCasingInFileNames": true,
|
|
8
|
+
"resolveJsonModule": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"moduleResolution": "bundler"
|
|
13
|
+
}
|
|
14
|
+
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
|
15
|
+
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
|
|
16
|
+
//
|
|
17
|
+
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
|
18
|
+
// from the referenced tsconfig.json - TypeScript does not merge them in
|
|
19
|
+
}
|