@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
@@ -18,12 +18,12 @@
18
18
  <div id="fmi-warnings-list">
19
19
  <Warning
20
20
  v-for="warning in warnings"
21
- :key="warning.key"
21
+ :key="warning.type"
22
22
  :input="warning"
23
23
  :hideable="warnings.length > 1"
24
24
  :theme="theme"
25
25
  :language="language"
26
- @warningToggled="onWarningToggled" />
26
+ @warning-toggled="onWarningToggled" />
27
27
  </div>
28
28
  <div class="row symbol-list-main-row">
29
29
  <hr
@@ -35,8 +35,7 @@
35
35
  <div class="symbol-list-cell symbol-list-cell-image">
36
36
  <div
37
37
  class="gray several symbol-list-image-column symbol-list-image warning-image"
38
- aria-labelledby="symbol-list-several-warnings-text">
39
- </div>
38
+ aria-labelledby="symbol-list-several-warnings-text"></div>
40
39
  </div>
41
40
  <div class="symbol-list-cell symbol-list-cell-text">
42
41
  <div
@@ -52,8 +51,7 @@
52
51
  <div class="symbol-list-cell symbol-list-cell-image">
53
52
  <div
54
53
  class="level-1 symbol-list-image-column symbol-list-image warning-image"
55
- aria-labelledby="symbol-list-warning-level-1-text">
56
- </div>
54
+ aria-labelledby="symbol-list-warning-level-1-text"></div>
57
55
  </div>
58
56
  <div class="symbol-list-cell symbol-list-cell-text">
59
57
  <div
@@ -69,8 +67,7 @@
69
67
  <div class="symbol-list-cell symbol-list-cell-image">
70
68
  <div
71
69
  class="level-2 symbol-list-image-column symbol-list-image warning-image"
72
- aria-labelledby="symbol-list-warning-level-2-text">
73
- </div>
70
+ aria-labelledby="symbol-list-warning-level-2-text"></div>
74
71
  </div>
75
72
  <div class="symbol-list-cell symbol-list-cell-text">
76
73
  <div
@@ -86,8 +83,7 @@
86
83
  <div class="symbol-list-cell symbol-list-cell-image">
87
84
  <div
88
85
  class="level-3 symbol-list-image-column symbol-list-image warning-image"
89
- aria-labelledby="symbol-list-warning-level-3-text">
90
- </div>
86
+ aria-labelledby="symbol-list-warning-level-3-text"></div>
91
87
  </div>
92
88
  <div class="symbol-list-cell symbol-list-cell-text">
93
89
  <div
@@ -103,8 +99,7 @@
103
99
  <div class="symbol-list-cell symbol-list-cell-image">
104
100
  <div
105
101
  class="level-4 symbol-list-image-column symbol-list-image warning-image"
106
- aria-labelledby="symbol-list-warning-level-4-text">
107
- </div>
102
+ aria-labelledby="symbol-list-warning-level-4-text"></div>
108
103
  </div>
109
104
  <div class="symbol-list-cell symbol-list-cell-text">
110
105
  <div
@@ -119,69 +114,138 @@
119
114
  </div>
120
115
  </template>
121
116
 
122
- <script>
123
- import i18n from '../mixins/i18n'
117
+ <script setup lang="ts">
118
+ import { computed, toRef } from 'vue'
119
+ import { useI18n } from '@/composables/useI18n'
124
120
  import Warning from './Warning.vue'
121
+ import type { LegendItem, Theme, Language } from '@/types'
122
+
123
+ // ============================================================================
124
+ // Props
125
+ // ============================================================================
126
+
127
+ const props = withDefaults(
128
+ defineProps<{
129
+ input?: LegendItem[]
130
+ visibleWarnings?: string[]
131
+ language?: Language
132
+ theme?: Theme | string
133
+ }>(),
134
+ {
135
+ input: () => [],
136
+ visibleWarnings: () => [],
137
+ language: undefined,
138
+ theme: 'light-theme',
139
+ }
140
+ )
125
141
 
126
- export default {
127
- name: 'Warnings',
128
- components: {
129
- Warning,
130
- },
131
- mixins: [i18n],
132
- props: ['input', 'visibleWarnings', 'language', 'theme'],
133
- computed: {
134
- warnings() {
135
- return this.input
136
- },
137
- hiddenWarnings() {
138
- return this.visibleWarnings.length !== this.input.length
139
- },
140
- noWarnings() {
141
- return this.warnings.length === 0
142
- },
143
- warningSymbolsText() {
144
- return this.noWarnings ? this.t('noWarnings') : this.t('warningSymbols')
145
- },
146
- warningSymbolDaysText() {
147
- return this.noWarnings ? '' : this.t('warningSymbolDays')
148
- },
149
- showWarningsText() {
150
- return this.t('showWarnings')
151
- },
152
- severalWarningsText() {
153
- return this.t('severalWarnings')
154
- },
155
- warningLevel1Text() {
156
- return this.t('warningLevel1')
157
- },
158
- warningLevel2Text() {
159
- return this.t('warningLevel2')
160
- },
161
- warningLevel3Text() {
162
- return this.t('warningLevel3')
163
- },
164
- warningLevel4Text() {
165
- return this.t('warningLevel4')
166
- },
167
- },
168
- methods: {
169
- onWarningToggled({ warning, visible }) {
170
- let newVisibleWarnings = this.visibleWarnings
171
- if (visible && !this.visibleWarnings.includes(warning)) {
172
- newVisibleWarnings.push(warning)
173
- } else if (!visible) {
174
- newVisibleWarnings = newVisibleWarnings.filter(
175
- (visibleWarning) => visibleWarning !== warning
176
- )
177
- }
178
- this.$emit('warningsToggled', newVisibleWarnings)
179
- },
180
- showAll() {
181
- this.$emit('showAllWarnings')
182
- },
183
- },
142
+ // ============================================================================
143
+ // Emits
144
+ // ============================================================================
145
+
146
+ const emit = defineEmits<{
147
+ warningsToggled: [visibleWarnings: string[]]
148
+ showAllWarnings: []
149
+ }>()
150
+
151
+ // ============================================================================
152
+ // Composables
153
+ // ============================================================================
154
+
155
+ const { t } = useI18n(toRef(() => props.language))
156
+
157
+ // ============================================================================
158
+ // Computed Properties
159
+ // ============================================================================
160
+
161
+ const warnings = computed<LegendItem[]>(() => {
162
+ return props.input
163
+ })
164
+
165
+ const hiddenWarnings = computed<boolean>(() => {
166
+ return props.visibleWarnings.length !== props.input.length
167
+ })
168
+
169
+ const noWarnings = computed<boolean>(() => {
170
+ return warnings.value.length === 0
171
+ })
172
+
173
+ const warningSymbolsText = computed<string>(() => {
174
+ return noWarnings.value ? t('noWarnings') : t('warningSymbols')
175
+ })
176
+
177
+ const warningSymbolDaysText = computed<string>(() => {
178
+ return noWarnings.value ? '' : t('warningSymbolDays')
179
+ })
180
+
181
+ const showWarningsText = computed<string>(() => {
182
+ return t('showWarnings')
183
+ })
184
+
185
+ const severalWarningsText = computed<string>(() => {
186
+ return t('severalWarnings')
187
+ })
188
+
189
+ const warningLevel1Text = computed<string>(() => {
190
+ return t('warningLevel1')
191
+ })
192
+
193
+ const warningLevel2Text = computed<string>(() => {
194
+ return t('warningLevel2')
195
+ })
196
+
197
+ const warningLevel3Text = computed<string>(() => {
198
+ return t('warningLevel3')
199
+ })
200
+
201
+ const warningLevel4Text = computed<string>(() => {
202
+ return t('warningLevel4')
203
+ })
204
+
205
+ // ============================================================================
206
+ // Methods
207
+ // ============================================================================
208
+
209
+ interface WarningToggleEvent {
210
+ warning: string
211
+ visible: boolean
184
212
  }
213
+
214
+ const onWarningToggled = ({ warning, visible }: WarningToggleEvent): void => {
215
+ let newVisibleWarnings = [...props.visibleWarnings]
216
+ if (visible && !props.visibleWarnings.includes(warning)) {
217
+ newVisibleWarnings.push(warning)
218
+ } else if (!visible) {
219
+ newVisibleWarnings = newVisibleWarnings.filter(
220
+ (visibleWarning) => visibleWarning !== warning
221
+ )
222
+ }
223
+ emit('warningsToggled', newVisibleWarnings)
224
+ }
225
+
226
+ const showAll = (): void => {
227
+ emit('showAllWarnings')
228
+ }
229
+
230
+ // ============================================================================
231
+ // Expose for tests
232
+ // ============================================================================
233
+
234
+ defineExpose({
235
+ warnings,
236
+ hiddenWarnings,
237
+ noWarnings,
238
+ warningSymbolsText,
239
+ warningSymbolDaysText,
240
+ showWarningsText,
241
+ severalWarningsText,
242
+ warningLevel1Text,
243
+ warningLevel2Text,
244
+ warningLevel3Text,
245
+ warningLevel4Text,
246
+ onWarningToggled,
247
+ showAll,
248
+ })
185
249
  </script>
186
250
 
187
251
  <style scoped lang="scss">
@@ -0,0 +1,360 @@
1
+ /**
2
+ * Core composable for AlertClient wrapper components.
3
+ * Contains shared logic for both web component (App.vue) and Vue component (AlertClientVue.vue).
4
+ *
5
+ * This composable provides:
6
+ * - Reactive state management (loading, warningsData, themeClass, etc.)
7
+ * - Computed properties for API queries
8
+ * - Methods for fetching warnings and handling events
9
+ *
10
+ * Components using this composable must provide:
11
+ * - Props with appropriate types
12
+ */
13
+ import { ref, computed, type Ref, type ComputedRef } from 'vue'
14
+ import crossFetch from 'cross-fetch'
15
+ import type { Language, WarningsData } from '@/types'
16
+
17
+ // ============================================================================
18
+ // Helper Functions (exported for use in components)
19
+ // ============================================================================
20
+
21
+ /**
22
+ * Normalize string|boolean to boolean
23
+ */
24
+ export const toBool = (val: unknown, defaultVal = true): boolean => {
25
+ if (typeof val === 'boolean') return val
26
+ if (typeof val === 'string') return val.toLowerCase() !== 'false'
27
+ return defaultVal
28
+ }
29
+
30
+ /**
31
+ * Normalize string|number to number
32
+ */
33
+ export const toNum = (val: unknown, defaultVal = 0): number => {
34
+ if (typeof val === 'number') return val
35
+ if (typeof val === 'string') return Number(val)
36
+ return defaultVal
37
+ }
38
+
39
+ // ============================================================================
40
+ // Types
41
+ // ============================================================================
42
+
43
+ export interface UseAlertClientOptions {
44
+ /** Base URL for API requests */
45
+ baseUrl: Ref<string> | ComputedRef<string>
46
+ /** Language code */
47
+ language: Ref<Language> | ComputedRef<Language>
48
+ /** Theme name */
49
+ theme: Ref<string> | ComputedRef<string>
50
+ /** Pre-loaded warnings data (optional) */
51
+ warnings?:
52
+ | Ref<WarningsData | string | null>
53
+ | ComputedRef<WarningsData | string | null>
54
+ /** Current date for time calculations (optional) */
55
+ currentDate?: Ref<Date | string | null> | ComputedRef<Date | string | null>
56
+ /** Font scale factor (optional) */
57
+ fontScale?: Ref<number | string> | ComputedRef<number | string>
58
+ /** Debug mode (optional) */
59
+ debugMode?: Ref<boolean> | ComputedRef<boolean>
60
+ /** Custom weather updated query (optional) */
61
+ weatherUpdated?: Ref<string> | ComputedRef<string>
62
+ /** Custom flood updated query (optional) */
63
+ floodUpdated?: Ref<string> | ComputedRef<string>
64
+ /** Custom weather warnings query (optional) */
65
+ weatherWarnings?: Ref<string> | ComputedRef<string>
66
+ /** Custom flood warnings query (optional) */
67
+ floodWarnings?: Ref<string> | ComputedRef<string>
68
+ }
69
+
70
+ export interface UseAlertClientReturn {
71
+ // State
72
+ loading: Ref<number>
73
+ updatedAt: Ref<number | null>
74
+ refreshedAt: Ref<number | null>
75
+ themeClass: Ref<string>
76
+ warningsData: Ref<WarningsData | null>
77
+ visible: Ref<boolean>
78
+
79
+ // Computed
80
+ currentTime: ComputedRef<number>
81
+ weatherUpdatedQuery: ComputedRef<string>
82
+ floodUpdatedQuery: ComputedRef<string>
83
+ weatherWarningsQuery: ComputedRef<string>
84
+ floodWarningsQuery: ComputedRef<string>
85
+
86
+ // Methods
87
+ onLoaded: (loaded: number) => void
88
+ onThemeChanged: (newTheme: string | null) => void
89
+ fetchWarnings: () => Promise<void> | undefined
90
+ show: () => void
91
+ hide: () => void
92
+ initializeWarnings: () => void
93
+ applyFontScale: () => void
94
+ }
95
+
96
+ // ============================================================================
97
+ // Constants
98
+ // ============================================================================
99
+
100
+ const WEATHER_UPDATED_TYPE = 'weather_update_time'
101
+ const FLOOD_UPDATED_TYPE = 'flood_update_time'
102
+ const WEATHER_WARNINGS_TYPE = 'weather_finland_active_all'
103
+ const FLOOD_WARNINGS_TYPE = 'flood_finland_active_all'
104
+ const FLOOD_SUPPORTED_SEVERITIES = ['moderate', 'severe', 'extreme'] as const
105
+
106
+ const QUERY_PREFIX =
107
+ '?service=WFS&version=1.0.0&request=GetFeature&maxFeatures=1000&outputFormat=application%2Fjson&typeName='
108
+
109
+ // ============================================================================
110
+ // Composable
111
+ // ============================================================================
112
+
113
+ export function useAlertClient(
114
+ options: UseAlertClientOptions
115
+ ): UseAlertClientReturn {
116
+ const {
117
+ baseUrl,
118
+ language,
119
+ theme,
120
+ warnings,
121
+ currentDate,
122
+ fontScale,
123
+ debugMode,
124
+ weatherUpdated,
125
+ floodUpdated,
126
+ weatherWarnings,
127
+ floodWarnings,
128
+ } = options
129
+
130
+ // -------------------------------------------------------------------------
131
+ // Reactive State
132
+ // -------------------------------------------------------------------------
133
+
134
+ const loading = ref<number>(1)
135
+ const updatedAt = ref<number | null>(null)
136
+ const refreshedAt = ref<number | null>(null)
137
+ const themeClass = ref<string>(`${theme.value}-theme`)
138
+ const warningsData = ref<WarningsData | null>(null)
139
+ const visible = ref<boolean>(true)
140
+
141
+ // -------------------------------------------------------------------------
142
+ // Helper: CAP Language mapping
143
+ // -------------------------------------------------------------------------
144
+
145
+ const capLanguageMap: Record<Language, string> = {
146
+ fi: 'fi-FI',
147
+ sv: 'sv-SV',
148
+ en: 'en-US',
149
+ }
150
+
151
+ const getCapLanguage = (): string => capLanguageMap[language.value] || 'fi-FI'
152
+
153
+ // -------------------------------------------------------------------------
154
+ // Computed: Flood filter
155
+ // -------------------------------------------------------------------------
156
+
157
+ const floodFilter = computed<string>(() => {
158
+ const severityFilter = FLOOD_SUPPORTED_SEVERITIES.reduce(
159
+ (filter, severity, index) =>
160
+ `${filter}${index === 0 ? '' : ','}%27${severity.toUpperCase()}%27`,
161
+ '&cql_filter=severity%20IN%20('
162
+ )
163
+ return `${severityFilter})%20AND%20language=%27${getCapLanguage()}%27`
164
+ })
165
+
166
+ // -------------------------------------------------------------------------
167
+ // Computed: Query URLs
168
+ // -------------------------------------------------------------------------
169
+
170
+ const weatherUpdatedQuery = computed<string>(() => {
171
+ return weatherUpdated?.value || `${QUERY_PREFIX}${WEATHER_UPDATED_TYPE}`
172
+ })
173
+
174
+ const floodUpdatedQuery = computed<string>(() => {
175
+ return floodUpdated?.value || `${QUERY_PREFIX}${FLOOD_UPDATED_TYPE}`
176
+ })
177
+
178
+ const weatherWarningsQuery = computed<string>(() => {
179
+ return weatherWarnings?.value || `${QUERY_PREFIX}${WEATHER_WARNINGS_TYPE}`
180
+ })
181
+
182
+ const floodWarningsQuery = computed<string>(() => {
183
+ return (
184
+ floodWarnings?.value ||
185
+ `${QUERY_PREFIX}${FLOOD_WARNINGS_TYPE}${floodFilter.value}`
186
+ )
187
+ })
188
+
189
+ // -------------------------------------------------------------------------
190
+ // Computed: Current time
191
+ // -------------------------------------------------------------------------
192
+
193
+ const currentTime = computed<number>(() => {
194
+ if (refreshedAt.value) {
195
+ return refreshedAt.value
196
+ }
197
+ if (currentDate?.value) {
198
+ const date =
199
+ currentDate.value instanceof Date
200
+ ? currentDate.value
201
+ : new Date(currentDate.value)
202
+ return date.getTime()
203
+ }
204
+ return Date.now()
205
+ })
206
+
207
+ // -------------------------------------------------------------------------
208
+ // Methods
209
+ // -------------------------------------------------------------------------
210
+
211
+ /**
212
+ * Initialize warnings from pre-loaded data (call in created/setup)
213
+ */
214
+ const initializeWarnings = (): void => {
215
+ if (warnings?.value) {
216
+ warningsData.value =
217
+ typeof warnings.value === 'string'
218
+ ? JSON.parse(warnings.value)
219
+ : warnings.value
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Apply font scale to document (call in onMounted)
225
+ */
226
+ const applyFontScale = (): void => {
227
+ const fontScaleNum = toNum(fontScale?.value, 1)
228
+ if (fontScaleNum !== 1) {
229
+ let originalFontSize: number | undefined
230
+
231
+ if (
232
+ typeof window !== 'undefined' &&
233
+ typeof document !== 'undefined' &&
234
+ document.documentElement &&
235
+ window.getComputedStyle
236
+ ) {
237
+ const htmlElement = document.documentElement
238
+ const computedStyle = window.getComputedStyle(htmlElement)
239
+ originalFontSize = parseFloat(computedStyle.fontSize)
240
+ }
241
+
242
+ if (originalFontSize == null || Number.isNaN(originalFontSize)) {
243
+ originalFontSize = 16 // Fallback
244
+ }
245
+
246
+ const scaledFontSize = fontScaleNum * originalFontSize
247
+ const newFontSize = Math.round(scaledFontSize * 100) / 100
248
+ document.documentElement.style.fontSize = `${newFontSize}px`
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Handle loaded event from child component
254
+ */
255
+ const onLoaded = (loaded: number): void => {
256
+ if (loaded !== 0) {
257
+ loading.value = loaded === -1 ? -1 : 0
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Handle theme change
263
+ */
264
+ const onThemeChanged = (newTheme: string | null): void => {
265
+ themeClass.value = `${
266
+ newTheme != null && newTheme.length > 0 ? newTheme : theme.value
267
+ }-theme`
268
+ }
269
+
270
+ /**
271
+ * Fetch warnings from API
272
+ */
273
+ const fetchWarnings = (): Promise<void> | undefined => {
274
+ if (warnings?.value) {
275
+ return
276
+ }
277
+
278
+ loading.value = 1
279
+
280
+ if (debugMode?.value) {
281
+ console.log(`Updating warnings at ${new Date()}`)
282
+ }
283
+
284
+ const queries = new Map<string, string>([
285
+ [`${baseUrl.value}${weatherUpdatedQuery.value}`, WEATHER_UPDATED_TYPE],
286
+ [`${baseUrl.value}${floodUpdatedQuery.value}`, FLOOD_UPDATED_TYPE],
287
+ [`${baseUrl.value}${weatherWarningsQuery.value}`, WEATHER_WARNINGS_TYPE],
288
+ [`${baseUrl.value}${floodWarningsQuery.value}`, FLOOD_WARNINGS_TYPE],
289
+ ])
290
+
291
+ const responseData: Record<string, unknown> = {}
292
+
293
+ return Promise.allSettled(
294
+ [...queries.keys()].map(async (queryUrl) =>
295
+ crossFetch(queryUrl).then((response) =>
296
+ response
297
+ .json()
298
+ .then((json: unknown) => {
299
+ const currentTimeMs = Date.now()
300
+ if (updatedAt.value != null) {
301
+ refreshedAt.value = currentTimeMs
302
+ }
303
+ updatedAt.value = currentTimeMs
304
+ responseData[queries.get(queryUrl)!] = json
305
+ })
306
+ .catch((error: Error) => {
307
+ loading.value = -1
308
+ console.log(error)
309
+ })
310
+ )
311
+ )
312
+ ).then(() => {
313
+ warningsData.value = responseData as WarningsData
314
+ })
315
+ }
316
+
317
+ /**
318
+ * Show the component
319
+ */
320
+ const show = (): void => {
321
+ visible.value = true
322
+ }
323
+
324
+ /**
325
+ * Hide the component
326
+ */
327
+ const hide = (): void => {
328
+ visible.value = false
329
+ }
330
+
331
+ // -------------------------------------------------------------------------
332
+ // Return
333
+ // -------------------------------------------------------------------------
334
+
335
+ return {
336
+ // State
337
+ loading,
338
+ updatedAt,
339
+ refreshedAt,
340
+ themeClass,
341
+ warningsData,
342
+ visible,
343
+
344
+ // Computed
345
+ currentTime,
346
+ weatherUpdatedQuery,
347
+ floodUpdatedQuery,
348
+ weatherWarningsQuery,
349
+ floodWarningsQuery,
350
+
351
+ // Methods
352
+ onLoaded,
353
+ onThemeChanged,
354
+ fetchWarnings,
355
+ show,
356
+ hide,
357
+ initializeWarnings,
358
+ applyFontScale,
359
+ }
360
+ }