@fmidev/smartmet-alert-client 4.4.19 → 4.7.0-beta.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.
Files changed (123) hide show
  1. package/.eslintignore +2 -14
  2. package/.github/workflows/test.yaml +26 -0
  3. package/.nvmrc +1 -0
  4. package/AGENTS.md +26 -0
  5. package/index.html +1 -1
  6. package/package.json +80 -22
  7. package/src/AlertClientVue.vue +160 -0
  8. package/src/App.vue +154 -296
  9. package/src/assets/img/ui/arrow-down.svg +4 -11
  10. package/src/assets/img/ui/arrow-up.svg +4 -11
  11. package/src/assets/img/ui/clear.svg +7 -21
  12. package/src/assets/img/ui/close.svg +4 -15
  13. package/src/assets/img/ui/toggle-selected.svg +5 -6
  14. package/src/assets/img/ui/toggle-unselected.svg +5 -6
  15. package/src/assets/img/warning/cold-weather.svg +3 -6
  16. package/src/assets/img/warning/flood-level-3.svg +4 -7
  17. package/src/assets/img/warning/forest-fire-weather.svg +2 -6
  18. package/src/assets/img/warning/grass-fire-weather.svg +2 -6
  19. package/src/assets/img/warning/hot-weather.svg +3 -6
  20. package/src/assets/img/warning/pedestrian-safety.svg +3 -7
  21. package/src/assets/img/warning/rain.svg +2 -7
  22. package/src/assets/img/warning/sea-icing.svg +2 -6
  23. package/src/assets/img/warning/sea-thunder-storm.svg +2 -5
  24. package/src/assets/img/warning/sea-water-height-high-water.svg +3 -8
  25. package/src/assets/img/warning/sea-water-height-shallow-water.svg +3 -7
  26. package/src/assets/img/warning/sea-wave-height.svg +4 -7
  27. package/src/assets/img/warning/sea-wind-legend.svg +2 -5
  28. package/src/assets/img/warning/sea-wind.svg +2 -5
  29. package/src/assets/img/warning/several.svg +2 -5
  30. package/src/assets/img/warning/thunder-storm.svg +2 -5
  31. package/src/assets/img/warning/traffic-weather.svg +2 -6
  32. package/src/assets/img/warning/uv-note.svg +2 -6
  33. package/src/assets/img/warning/wind.svg +2 -5
  34. package/src/components/AlertClient.vue +330 -251
  35. package/src/components/CollapsiblePanel.vue +281 -0
  36. package/src/components/DayLarge.vue +146 -110
  37. package/src/components/DaySmall.vue +97 -81
  38. package/src/components/Days.vue +229 -159
  39. package/src/components/DescriptionWarning.vue +63 -38
  40. package/src/components/GrayScaleToggle.vue +58 -54
  41. package/src/components/Legend.vue +102 -325
  42. package/src/components/MapLarge.vue +574 -351
  43. package/src/components/MapSmall.vue +137 -122
  44. package/src/components/PopupRow.vue +24 -12
  45. package/src/components/Region.vue +168 -118
  46. package/src/components/RegionWarning.vue +40 -33
  47. package/src/components/Regions.vue +189 -105
  48. package/src/components/Warning.vue +70 -45
  49. package/src/components/Warnings.vue +136 -72
  50. package/src/composables/useAlertClient.ts +360 -0
  51. package/src/composables/useConfig.ts +573 -0
  52. package/src/composables/useFields.ts +66 -0
  53. package/src/composables/useI18n.ts +62 -0
  54. package/src/composables/useKeyCodes.ts +16 -0
  55. package/src/composables/useMapPaths.ts +477 -0
  56. package/src/composables/useUtils.ts +683 -0
  57. package/src/composables/useWarningsProcessor.ts +1007 -0
  58. package/src/data/geometries.json +993 -0
  59. package/src/{main.js → main.ts} +1 -0
  60. package/src/mixins/geojsonsvg.d.ts +57 -0
  61. package/src/mixins/geojsonsvg.js +5 -3
  62. package/src/plugins/index.ts +5 -0
  63. package/src/scss/_utilities.scss +193 -0
  64. package/src/scss/constants.scss +2 -1
  65. package/src/scss/warningImages.scss +8 -3
  66. package/src/types/index.ts +509 -0
  67. package/src/vite-env.d.ts +23 -0
  68. package/src/vue.ts +41 -0
  69. package/svgo.config.js +45 -0
  70. package/tests/README.md +430 -0
  71. package/tests/fixtures/mockWarningData.ts +152 -0
  72. package/tests/integration/warning-flow.spec.ts +445 -0
  73. package/tests/setup.ts +41 -0
  74. package/tests/unit/components/AlertClient.spec.ts +701 -0
  75. package/tests/unit/components/DayLarge.spec.ts +348 -0
  76. package/tests/unit/components/DaySmall.spec.ts +352 -0
  77. package/tests/unit/components/Days.spec.ts +548 -0
  78. package/tests/unit/components/DescriptionWarning.spec.ts +385 -0
  79. package/tests/unit/components/GrayScaleToggle.spec.ts +318 -0
  80. package/tests/unit/components/Legend.spec.ts +295 -0
  81. package/tests/unit/components/MapLarge.spec.ts +448 -0
  82. package/tests/unit/components/MapSmall.spec.ts +367 -0
  83. package/tests/unit/components/PopupRow.spec.ts +270 -0
  84. package/tests/unit/components/Region.spec.ts +373 -0
  85. package/tests/unit/components/RegionWarning.snapshot.spec.ts +361 -0
  86. package/tests/unit/components/RegionWarning.spec.ts +381 -0
  87. package/tests/unit/components/Regions.spec.ts +503 -0
  88. package/tests/unit/components/Warning.snapshot.spec.ts +483 -0
  89. package/tests/unit/components/Warning.spec.ts +489 -0
  90. package/tests/unit/components/Warnings.spec.ts +343 -0
  91. package/tests/unit/components/__snapshots__/RegionWarning.snapshot.spec.ts.snap +41 -0
  92. package/tests/unit/components/__snapshots__/Warning.snapshot.spec.ts.snap +433 -0
  93. package/tests/unit/composables/useConfig.spec.ts +279 -0
  94. package/tests/unit/composables/useI18n.spec.ts +116 -0
  95. package/tests/unit/composables/useKeyCodes.spec.ts +27 -0
  96. package/tests/unit/composables/useUtils.spec.ts +213 -0
  97. package/tsconfig.json +43 -0
  98. package/tsconfig.node.json +11 -0
  99. package/vite.config.js +96 -26
  100. package/vitest.config.js +40 -0
  101. package/dist/favicon.ico +0 -0
  102. package/dist/index.dark.html +0 -20
  103. package/dist/index.en.html +0 -15
  104. package/dist/index.fi.html +0 -15
  105. package/dist/index.html +0 -15
  106. package/dist/index.js +0 -281
  107. package/dist/index.mjs +0 -281
  108. package/dist/index.mjs.map +0 -1
  109. package/dist/index.relative.html +0 -19
  110. package/dist/index.start.html +0 -20
  111. package/dist/index.sv.html +0 -15
  112. package/playwright.config.ts +0 -18
  113. package/public/index.relative.html +0 -19
  114. package/public/index.start.html +0 -20
  115. package/src/mixins/config.js +0 -1378
  116. package/src/mixins/fields.js +0 -26
  117. package/src/mixins/i18n.js +0 -25
  118. package/src/mixins/keycodes.js +0 -10
  119. package/src/mixins/panzoom.js +0 -900
  120. package/src/mixins/utils.js +0 -900
  121. package/src/plugins/index.js +0 -3
  122. package/test/snapshot.test.ts +0 -126
  123. package/vitest.config.ts +0 -6
@@ -0,0 +1,281 @@
1
+ <template>
2
+ <div :class="['collapsible-panel', theme, 'mobile-only']">
3
+ <div class="panel-header">
4
+ <div class="panel-title">
5
+ <span class="panel-text">
6
+ {{ title }}
7
+ </span>
8
+ </div>
9
+ <button
10
+ :class="['panel-toggle', visible ? '' : 'collapsed']"
11
+ :aria-expanded="visible"
12
+ :aria-label="title"
13
+ @click="onToggle"></button>
14
+ </div>
15
+ <Transition name="collapse">
16
+ <div v-if="visible" class="panel-body">
17
+ <div class="panel-content">
18
+ <slot></slot>
19
+ </div>
20
+ </div>
21
+ </Transition>
22
+ </div>
23
+ </template>
24
+
25
+ <script setup lang="ts">
26
+ // Props
27
+ withDefaults(
28
+ defineProps<{
29
+ visible?: boolean
30
+ title?: string
31
+ theme?: string
32
+ }>(),
33
+ {
34
+ visible: false,
35
+ title: '',
36
+ theme: 'light-theme',
37
+ }
38
+ )
39
+
40
+ // Emits
41
+ const emit = defineEmits<{
42
+ toggle: []
43
+ }>()
44
+
45
+ // Methods
46
+ function onToggle(): void {
47
+ emit('toggle')
48
+ }
49
+ </script>
50
+
51
+ <style scoped lang="scss">
52
+ @import '../scss/constants.scss';
53
+
54
+ .collapsible-panel {
55
+ margin-left: 0;
56
+ margin-right: 0;
57
+ margin-bottom: 0.25rem;
58
+ border-radius: 0;
59
+ }
60
+
61
+ .light-theme.collapsible-panel {
62
+ border: 0.5px solid $light-legend-background-color;
63
+ }
64
+
65
+ .dark-theme.collapsible-panel {
66
+ border: 0.5px solid $dark-legend-background-color;
67
+ }
68
+
69
+ .light-gray-theme.collapsible-panel {
70
+ border: 0.5px solid $light-gray-legend-background-color;
71
+ }
72
+
73
+ .dark-gray-theme.collapsible-panel {
74
+ border: 0.5px solid $dark-gray-legend-background-color;
75
+ }
76
+
77
+ .panel-header {
78
+ height: $current-warning-height;
79
+ padding: 0 0 0 15px;
80
+ line-height: $current-warning-height;
81
+ border: none;
82
+ border-bottom: 0.5px solid rgba(0, 0, 0, 0.125);
83
+ border-radius: 0;
84
+ position: relative;
85
+ }
86
+
87
+ .light-theme .panel-header {
88
+ background-color: $light-legend-heading-background-color;
89
+ }
90
+
91
+ .dark-theme .panel-header {
92
+ background-color: $dark-legend-heading-background-color;
93
+ }
94
+
95
+ .light-gray-theme .panel-header {
96
+ background-color: $light-gray-legend-heading-background-color;
97
+ }
98
+
99
+ .dark-gray-theme .panel-header {
100
+ background-color: $dark-gray-legend-heading-background-color;
101
+ }
102
+
103
+ .panel-title {
104
+ position: absolute;
105
+ left: 0;
106
+ right: 38px;
107
+ border-radius: 0;
108
+ display: flex;
109
+ align-items: center;
110
+ justify-content: start;
111
+ height: $current-warning-height;
112
+ }
113
+
114
+ .light-theme .panel-title {
115
+ background: $light-legend-heading-background-color;
116
+ }
117
+
118
+ .dark-theme .panel-title {
119
+ background: $dark-legend-heading-background-color;
120
+ }
121
+
122
+ .light-gray-theme .panel-title {
123
+ background: $light-gray-legend-heading-background-color;
124
+ }
125
+
126
+ .dark-gray-theme .panel-title {
127
+ background: $dark-gray-legend-heading-background-color;
128
+ }
129
+
130
+ .panel-text {
131
+ display: block;
132
+ text-align: left;
133
+ white-space: nowrap;
134
+ overflow: hidden;
135
+ text-overflow: ellipsis;
136
+ margin-left: 15px;
137
+ }
138
+
139
+ .light-theme .panel-text {
140
+ background-color: $light-legend-heading-background-color;
141
+ }
142
+
143
+ .dark-theme .panel-text {
144
+ background-color: $dark-legend-heading-background-color;
145
+ }
146
+
147
+ .light-gray-theme .panel-text {
148
+ background-color: $light-gray-legend-heading-background-color;
149
+ }
150
+
151
+ .dark-gray-theme .panel-text {
152
+ background-color: $dark-gray-legend-heading-background-color;
153
+ }
154
+
155
+ button.panel-toggle {
156
+ position: relative;
157
+ height: $current-warning-height;
158
+ width: $current-warning-height;
159
+ min-width: $current-warning-height;
160
+ background-image: url($ui-image-path + 'arrow-up.svg');
161
+ background-repeat: no-repeat;
162
+ background-position: center;
163
+ border-radius: 0;
164
+ border-style: none;
165
+ float: right;
166
+ padding: $image-padding;
167
+ margin-left: 5px;
168
+ cursor: pointer;
169
+
170
+ &.collapsed {
171
+ background-image: url($ui-image-path + 'arrow-down.svg');
172
+ border-radius: 0 3px 3px 0;
173
+ }
174
+ }
175
+
176
+ .light-theme .panel-toggle {
177
+ background-color: $light-legend-toggle-background-color;
178
+
179
+ &:hover {
180
+ background-color: $light-legend-toggle-background-color;
181
+ }
182
+
183
+ &:active {
184
+ background-color: $light-current-warning-toggle-active-color;
185
+ }
186
+ }
187
+
188
+ .dark-theme .panel-toggle {
189
+ background-color: $dark-legend-toggle-background-color;
190
+
191
+ &:hover {
192
+ background-color: $dark-legend-toggle-background-color;
193
+ }
194
+
195
+ &:active {
196
+ background-color: $dark-current-warning-toggle-active-color;
197
+ }
198
+ }
199
+
200
+ .light-gray-theme .panel-toggle {
201
+ background-color: $light-gray-legend-toggle-background-color;
202
+
203
+ &:hover {
204
+ background-color: $light-gray-legend-toggle-background-color;
205
+ }
206
+
207
+ &:active {
208
+ background-color: $light-gray-current-warning-toggle-active-color;
209
+ }
210
+ }
211
+
212
+ .dark-gray-theme .panel-toggle {
213
+ background-color: $dark-gray-legend-toggle-background-color;
214
+
215
+ &:hover {
216
+ background-color: $dark-gray-legend-toggle-background-color;
217
+ }
218
+
219
+ &:active {
220
+ background-color: $dark-gray-current-warning-toggle-active-color;
221
+ }
222
+ }
223
+
224
+ .panel-body {
225
+ overflow: hidden;
226
+ }
227
+
228
+ .panel-content {
229
+ padding: 15px;
230
+ border-radius: 0;
231
+ }
232
+
233
+ .light-theme .panel-content {
234
+ background-color: $light-legend-container-background-color;
235
+ border-top: 0.5px solid $light-legend-background-color;
236
+ }
237
+
238
+ .dark-theme .panel-content {
239
+ background-color: $dark-legend-container-background-color;
240
+ border-top: 0.5px solid $dark-legend-background-color;
241
+ }
242
+
243
+ .light-gray-theme .panel-content {
244
+ background-color: $light-gray-legend-container-background-color;
245
+ border-top: 0.5px solid $light-gray-legend-background-color;
246
+ }
247
+
248
+ .dark-gray-theme .panel-content {
249
+ background-color: $dark-gray-legend-container-background-color;
250
+ border-top: 0.5px solid $dark-gray-legend-background-color;
251
+ }
252
+
253
+ // Collapse transition
254
+ .collapse-enter-active,
255
+ .collapse-leave-active {
256
+ transition:
257
+ max-height 0.35s ease,
258
+ opacity 0.35s ease;
259
+ overflow: hidden;
260
+ }
261
+
262
+ .collapse-enter-from {
263
+ max-height: 0;
264
+ opacity: 0;
265
+ }
266
+
267
+ .collapse-enter-to {
268
+ max-height: 2000px;
269
+ opacity: 1;
270
+ }
271
+
272
+ .collapse-leave-from {
273
+ max-height: 2000px;
274
+ opacity: 1;
275
+ }
276
+
277
+ .collapse-leave-to {
278
+ max-height: 0;
279
+ opacity: 0;
280
+ }
281
+ </style>
@@ -34,121 +34,157 @@
34
34
  </div>
35
35
  </template>
36
36
 
37
- <script>
38
- import i18n from '../mixins/i18n'
39
- import utils from '../mixins/utils'
37
+ <script setup lang="ts">
38
+ import { computed, toRef } from 'vue'
39
+ import { useI18n } from '@/composables/useI18n'
40
+ import { twoDigits } from '@/composables/useUtils'
40
41
  import MapLarge from './MapLarge.vue'
42
+ import type { Day, DayRegions, WarningsMap, Theme, Language } from '@/types'
41
43
 
42
- export default {
43
- name: 'DayLarge',
44
- components: { MapLarge },
45
- mixins: [i18n, utils],
46
- props: {
47
- index: {
48
- type: Number,
49
- },
50
- input: {
51
- type: Object,
52
- default: () => ({}),
53
- },
54
- visibleWarnings: {
55
- type: Array,
56
- default: () => [],
57
- },
58
- warnings: {
59
- type: Object,
60
- default: null,
61
- },
62
- regions: {
63
- type: Object,
64
- },
65
- geometryId: {
66
- type: Number,
67
- },
68
- staticDays: {
69
- type: Boolean,
70
- default: true,
71
- },
72
- timeOffset: {
73
- type: Number,
74
- default: 0,
75
- },
76
- loading: {
77
- type: Boolean,
78
- default: true,
79
- },
80
- theme: {
81
- type: String,
82
- },
83
- language: {
84
- type: String,
85
- },
86
- spinnerEnabled: {
87
- type: Boolean,
88
- default: true,
89
- },
90
- },
91
- computed: {
92
- warningsTitle() {
93
- return this.t('warnings') || ''
94
- },
95
- updatedTitle() {
96
- return this.t('updated') || ''
97
- },
98
- atTime() {
99
- return this.t('atTime') || ''
100
- },
101
- warningsDate() {
102
- if (
103
- this.input.day == null ||
104
- this.input.month == null ||
105
- this.input.year == null
106
- ) {
107
- return ''
108
- }
109
- if (this.staticDays) {
110
- return `${this.input.day}.${this.input.month}.${this.input.year}`
111
- }
112
- const date = new Date(
113
- this.input.year,
114
- this.input.month - 1,
115
- this.input.day
116
- )
117
- const nextDate = new Date(date.getTime())
118
- nextDate.setDate(nextDate.getDate() + 1)
119
- const offset = this.timeOffset
120
- const offsetDate = new Date(date.getTime())
121
- offsetDate.setMilliseconds(offset)
122
- const hours = this.twoDigits(offsetDate.getHours())
123
- const minutes = this.twoDigits(offsetDate.getMinutes())
124
- return `${this.input.day}.${this.input.month}.${this.input.year} ${
125
- this.atTime
126
- } ${hours}:${minutes} –
44
+ // ============================================================================
45
+ // Props
46
+ // ============================================================================
47
+
48
+ const props = withDefaults(
49
+ defineProps<{
50
+ index: number
51
+ input?: Day
52
+ visibleWarnings?: string[]
53
+ warnings?: WarningsMap | null
54
+ regions?: DayRegions
55
+ geometryId?: number
56
+ staticDays?: boolean
57
+ timeOffset?: number
58
+ loading?: boolean
59
+ theme?: Theme | string
60
+ language?: Language
61
+ spinnerEnabled?: boolean
62
+ }>(),
63
+ {
64
+ input: () => ({}) as Day,
65
+ visibleWarnings: () => [],
66
+ warnings: null,
67
+ regions: undefined,
68
+ geometryId: undefined,
69
+ staticDays: true,
70
+ timeOffset: 0,
71
+ loading: true,
72
+ theme: undefined,
73
+ language: undefined,
74
+ spinnerEnabled: true,
75
+ }
76
+ )
77
+
78
+ // ============================================================================
79
+ // Emits
80
+ // ============================================================================
81
+
82
+ const emit = defineEmits<{
83
+ loaded: [value: boolean]
84
+ }>()
85
+
86
+ // ============================================================================
87
+ // Composables
88
+ // ============================================================================
89
+
90
+ const { t } = useI18n(toRef(() => props.language))
91
+
92
+ // ============================================================================
93
+ // Computed Properties
94
+ // ============================================================================
95
+
96
+ const warningsTitle = computed<string>(() => {
97
+ return t('warnings') || ''
98
+ })
99
+
100
+ const updatedTitle = computed<string>(() => {
101
+ return t('updated') || ''
102
+ })
103
+
104
+ const atTime = computed<string>(() => {
105
+ return t('atTime') || ''
106
+ })
107
+
108
+ const warningsDate = computed<string>(() => {
109
+ if (
110
+ props.input?.day == null ||
111
+ props.input?.month == null ||
112
+ props.input?.year == null
113
+ ) {
114
+ return ''
115
+ }
116
+
117
+ if (props.staticDays) {
118
+ return `${props.input.day}.${props.input.month}.${props.input.year}`
119
+ }
120
+
121
+ const date = new Date(
122
+ props.input.year,
123
+ props.input.month - 1,
124
+ props.input.day
125
+ )
126
+ const nextDate = new Date(date.getTime())
127
+ nextDate.setDate(nextDate.getDate() + 1)
128
+
129
+ const offset = props.timeOffset
130
+ const offsetDate = new Date(date.getTime())
131
+ offsetDate.setMilliseconds(offset)
132
+
133
+ const hours = twoDigits(offsetDate.getHours())
134
+ const minutes = twoDigits(offsetDate.getMinutes())
135
+
136
+ return `${props.input.day}.${props.input.month}.${props.input.year} ${
137
+ atTime.value
138
+ } ${hours}:${minutes} –
127
139
  <br> ${nextDate.getDate()}.${
128
140
  nextDate.getMonth() + 1
129
- }.${nextDate.getFullYear()} ${this.atTime} ${hours}:${minutes}`
130
- },
131
- updatedDate() {
132
- return this.input.updatedDate || ''
133
- },
134
- updatedTime() {
135
- return this.input.updatedTime || ''
136
- },
137
- dataProviderFirst() {
138
- return this.t('dataProviderFirst')
139
- },
140
- dataProviderSecond() {
141
- return this.t('dataProviderSecond')
142
- },
143
- },
144
- methods: {
145
- onLoaded(loaded) {
146
- if (loaded) {
147
- this.$emit('loaded', true)
148
- }
149
- },
150
- },
141
+ }.${nextDate.getFullYear()} ${atTime.value} ${hours}:${minutes}`
142
+ })
143
+
144
+ const updatedDate = computed<string>(() => {
145
+ return props.input?.updatedDate || ''
146
+ })
147
+
148
+ const updatedTime = computed<string>(() => {
149
+ return props.input?.updatedTime || ''
150
+ })
151
+
152
+ const dataProviderFirst = computed<string>(() => {
153
+ return t('dataProviderFirst')
154
+ })
155
+
156
+ const dataProviderSecond = computed<string>(() => {
157
+ return t('dataProviderSecond')
158
+ })
159
+
160
+ // ============================================================================
161
+ // Methods
162
+ // ============================================================================
163
+
164
+ const onLoaded = (loaded: boolean): void => {
165
+ if (loaded) {
166
+ emit('loaded', true)
167
+ }
151
168
  }
169
+
170
+ // ============================================================================
171
+ // Expose for tests
172
+ // ============================================================================
173
+
174
+ defineExpose({
175
+ warningsTitle,
176
+ updatedTitle,
177
+ atTime,
178
+ warningsDate,
179
+ updatedDate,
180
+ updatedTime,
181
+ dataProviderFirst,
182
+ dataProviderSecond,
183
+ onLoaded,
184
+ // Props exposed for tests
185
+ input: computed(() => props.input),
186
+ spinnerEnabled: computed(() => props.spinnerEnabled),
187
+ })
152
188
  </script>
153
189
 
154
190
  <style scoped lang="scss">