@dative-gpi/foundation-shared-components 0.0.213 → 0.0.215

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.
Binary file
Binary file
@@ -164,7 +164,7 @@ export default defineComponent({
164
164
 
165
165
  const menu = ref(false);
166
166
 
167
- const innerColor = ref(props.modelValue.toString().substring(0, 7));
167
+ const innerColor = ref(getColors(props.modelValue).base);
168
168
  const innerOpacity = ref(getHexFromPercentage(props.opacityValue));
169
169
  const fullColor = ref(innerColor.value + innerOpacity.value);
170
170
 
@@ -174,14 +174,14 @@ export default defineComponent({
174
174
  "--fs-color-field-cursor" : "default",
175
175
  "--fs-color-field-border-color" : lights.base,
176
176
  "--fs-color-field-color" : lights.dark,
177
- "--fs-color-field-colorvalue": fullColor.value,
177
+ "--fs-color-field-colorvalue" : fullColor.value,
178
178
  };
179
179
  }
180
180
  return {
181
181
  "--fs-color-field-cursor" : "pointer",
182
182
  "--fs-color-field-border-color" : lights.dark,
183
183
  "--fs-color-field-color" : darks.base,
184
- "--fs-color-field-colorvalue": fullColor.value,
184
+ "--fs-color-field-colorvalue" : fullColor.value,
185
185
  };
186
186
  });
187
187
 
@@ -12,29 +12,30 @@
12
12
  <FSRow>
13
13
  <FSColorField
14
14
  v-for="colorIndex in $props.colorCount"
15
- :key="colorIndex"
16
- :modelValue="$props.modelValue[colorIndex-1]"
15
+ :allowOpacity="$props.allowOpacity"
16
+ :modelValue="$props.modelValue[colorIndex - 1]"
17
17
  :required="$props.required"
18
18
  :editable="$props.editable"
19
- @update:modelValue="($event, index) => $emit('update:modelValue', $props.modelValue.map((color, i) => colorIndex === i + 1 ? $event : color))"
19
+ :key="colorIndex"
20
+ @update:modelValue="$emit('update:modelValue', $props.modelValue.map((color, i) => colorIndex === i + 1 ? $event : color))"
20
21
  />
21
22
  </FSRow>
22
23
  <FSSelectField
23
24
  class="fs-gradient-select-field"
24
- :items="items"
25
- @update:modelValue="$emit('update:modelValue', JSON.parse($event))"
26
25
  :clearable="false"
27
26
  :editable="$props.editable"
27
+ :items="items"
28
28
  :modelValue="JSON.stringify($props.modelValue)"
29
+ @update:modelValue="$emit('update:modelValue', presetGradients[$event])"
29
30
  >
30
31
  <template
31
32
  v-slot:selection="{ item }"
32
33
  >
33
34
  <FSRow
35
+ class="fs-gradient-field-preview"
34
36
  height="fill"
35
37
  width="100%"
36
- class="fs-gradient-field-preview"
37
- :style="{ '--fs-gradient-field-background': `linear-gradient(to right, ${JSON.parse(item.value).join(', ')})` }"
38
+ :style="{ '--fs-gradient-field-background': `linear-gradient(to right, ${encodeGradientCssColors(JSON.parse(item.value))})` }"
38
39
  >
39
40
  <span />
40
41
  </FSRow>
@@ -49,10 +50,10 @@
49
50
  #title
50
51
  >
51
52
  <FSRow
53
+ class="fs-gradient-field-preview"
52
54
  height="fill"
53
55
  width="100%"
54
- class="fs-gradient-field-preview"
55
- :style="{ '--fs-gradient-field-background': `linear-gradient(to right, ${JSON.parse(item.value).join(', ')})` }"
56
+ :style="{ '--fs-gradient-field-background': `linear-gradient(to right, ${encodeGradientCssColors(presetGradients[item.value])})` }"
56
57
  >
57
58
  <span />
58
59
  </FSRow>
@@ -67,21 +68,23 @@
67
68
  <script lang="ts">
68
69
  import { type PropType, defineComponent } from "vue";
69
70
 
71
+ import { groupedGradients } from "../../utils";
72
+ import { useColors } from "../../composables";
73
+
74
+ import FSSelectField from "./FSSelectField.vue";
70
75
  import FSColorField from "./FSColorField.vue";
76
+ import FSBaseField from "./FSBaseField.vue";
71
77
  import FSCol from "../FSCol.vue";
72
78
  import FSRow from "../FSRow.vue";
73
- import FSBaseField from "./FSBaseField.vue";
74
- import FSSelectField from "./FSSelectField.vue";
75
- import { groupedGradients } from "../../utils";
76
79
 
77
80
  export default defineComponent({
78
81
  name: "FSGradientField",
79
82
  components: {
80
- FSBaseField,
83
+ FSSelectField,
81
84
  FSColorField,
85
+ FSBaseField,
82
86
  FSCol,
83
- FSRow,
84
- FSSelectField
87
+ FSRow
85
88
  },
86
89
  props: {
87
90
  label: {
@@ -94,6 +97,11 @@ export default defineComponent({
94
97
  required: false,
95
98
  default: null
96
99
  },
100
+ colorCount: {
101
+ type: Number,
102
+ required: false,
103
+ default: 2
104
+ },
97
105
  modelValue: {
98
106
  type: Array as PropType<string[]>,
99
107
  required: true
@@ -108,18 +116,27 @@ export default defineComponent({
108
116
  required: false,
109
117
  default: true
110
118
  },
111
- colorCount: {
112
- type: Number,
119
+ allowOpacity: {
120
+ type: Boolean,
113
121
  required: false,
114
- default: 2
122
+ default: false
115
123
  }
116
124
  },
117
125
  emits: ["update:modelValue"],
118
126
  setup(props) {
119
- const items = groupedGradients[props.colorCount] ?? [];
127
+ const { getColors } = useColors();
128
+
129
+ const presetGradients = groupedGradients[props.colorCount];
130
+ const items = Object.keys(presetGradients)
131
+
132
+ const encodeGradientCssColors = (colors: string[]) => {
133
+ return colors.map((color) => getColors(color).base).join(", ");
134
+ };
120
135
 
121
136
  return {
122
- items
137
+ presetGradients,
138
+ items,
139
+ encodeGradientCssColors
123
140
  };
124
141
  }
125
142
  });
@@ -1,6 +1,7 @@
1
1
  <template>
2
2
  <FSCard
3
3
  :width="$props.width"
4
+ :style="style"
4
5
  v-bind="$attrs"
5
6
  >
6
7
  <FSCol
@@ -8,27 +9,28 @@
8
9
  width="fill"
9
10
  :class="['fs-map', { 'fs-map-fullscreen': fullScreen }]"
10
11
  >
11
- <FSCol
12
- v-if="$slots.leftoverlay"
13
- class="fs-map-overlay-left"
14
- width="hug"
15
- gap="2px"
12
+ <FSMapOverlay
13
+ v-if="$slots['leftoverlay-header'] || $slots['leftoverlay-body']"
14
+ :mode="$props.overlayMode"
15
+ :height="$props.height"
16
+ :mapId="mapId"
17
+ @update:mode="$emit('update:overlayMode', $event)"
16
18
  >
17
- <FSCard
18
- padding="4px"
19
- :elevation="true"
20
- :border="false"
19
+ <template
20
+ v-slot:leftoverlay-header
21
21
  >
22
- <FSFadeOut
23
- maskHeight="0"
24
- :height="`calc(${$props.height} - 40px)`"
25
- >
26
- <slot
27
- name="leftoverlay"
28
- />
29
- </FSFadeOut>
30
- </FSCard>
31
- </FSCol>
22
+ <slot
23
+ name="leftoverlay-header"
24
+ />
25
+ </template>
26
+ <template
27
+ v-slot:leftoverlay-body
28
+ >
29
+ <slot
30
+ name="leftoverlay-body"
31
+ />
32
+ </template>
33
+ </FSMapOverlay>
32
34
  <FSRow
33
35
  v-if="$props.editable && !editingLocation && $props.selectedLocationId !== null"
34
36
  class="fs-map-overlay-edit-button"
@@ -39,9 +41,7 @@
39
41
  @click="editingLocation = true"
40
42
  />
41
43
  </FSRow>
42
- <FSCol
43
- :style="style"
44
- >
44
+ <FSCol>
45
45
  <div
46
46
  class="fs-leaflet-container"
47
47
  :id="mapId"
@@ -129,21 +129,21 @@
129
129
  </template>
130
130
 
131
131
  <script lang="ts">
132
- import { computed, defineComponent, onMounted, type PropType, ref, watch } from "vue";
132
+ import { computed, defineComponent, onMounted, onUnmounted, type PropType, ref, watch } from "vue";
133
133
 
134
134
  import * as L from "leaflet";
135
135
  import "leaflet.markercluster";
136
136
 
137
137
  import { useTranslations as useTranslationsProvider } from "@dative-gpi/bones-ui/composables";
138
-
139
138
  import { type Address, type FSArea } from '@dative-gpi/foundation-shared-domain/models';
140
139
 
140
+ import { clusterMarker, locationMarker, myLocationMarker } from "../../utils";
141
141
  import { ColorEnum, type FSLocation, type MapLayer } from "../../models";
142
- import { useColors, useAddress } from "../../composables";
142
+ import { useColors, useAddress, useBreakpoints } from "../../composables";
143
143
 
144
144
  import FSMapEditPointAddressOverlay from "./FSMapEditPointAddressOverlay.vue";
145
145
  import FSMapLayerButton from "./FSMapLayerButton.vue";
146
- import FSFadeOut from "../FSFadeOut.vue";
146
+ import FSMapOverlay from "./FSMapOverlay.vue";
147
147
  import FSButton from "../FSButton.vue";
148
148
  import FSCard from "../FSCard.vue";
149
149
  import FSCol from "../FSCol.vue";
@@ -154,7 +154,7 @@ export default defineComponent({
154
154
  components: {
155
155
  FSMapEditPointAddressOverlay,
156
156
  FSMapLayerButton,
157
- FSFadeOut,
157
+ FSMapOverlay,
158
158
  FSButton,
159
159
  FSCard,
160
160
  FSCol,
@@ -181,6 +181,11 @@ export default defineComponent({
181
181
  required: false,
182
182
  default: false
183
183
  },
184
+ overlayMode: {
185
+ type: String as PropType<'collapse' | 'half' | 'expand'>,
186
+ required: false,
187
+ default: 'collapse'
188
+ },
184
189
  showMyLocation: {
185
190
  type: Boolean,
186
191
  required: false,
@@ -196,6 +201,11 @@ export default defineComponent({
196
201
  required: false,
197
202
  default: false
198
203
  },
204
+ enableScrollWheelZoom: {
205
+ type: Boolean,
206
+ required: false,
207
+ default: false
208
+ },
199
209
  center: {
200
210
  type: Array as PropType<number[]>,
201
211
  required: false,
@@ -212,14 +222,14 @@ export default defineComponent({
212
222
  default: () => [],
213
223
  },
214
224
  selectedLayer: {
215
- type: String as PropType<"osm" | "imagery">,
225
+ type: String as PropType<"map" | "imagery">,
216
226
  required: false,
217
- default: "osm"
227
+ default: "map"
218
228
  },
219
229
  selectableLayers: {
220
230
  type: Array as PropType<string[]>,
221
231
  required: false,
222
- default: () => ["osm", "imagery"]
232
+ default: () => ["map", "imagery"]
223
233
  },
224
234
  selectedLocationId: {
225
235
  type: String as PropType<string | null>,
@@ -232,11 +242,12 @@ export default defineComponent({
232
242
  default: null
233
243
  }
234
244
  },
235
- emits: ["update:modelValue", "update:selectedLocationId", "update:selectedAreaId"],
245
+ emits: ["update:modelValue", "update:selectedLocationId", "update:selectedAreaId", 'update:overlayMode'],
236
246
  setup(props, { emit }) {
247
+ const { $tr } = useTranslationsProvider();
237
248
  const { reverseSearch } = useAddress();
238
249
  const { getColors } = useColors();
239
- const { $tr } = useTranslationsProvider();
250
+ const { isExtraSmall } = useBreakpoints();
240
251
 
241
252
  const LL = window.L;
242
253
 
@@ -244,6 +255,9 @@ export default defineComponent({
244
255
  const innerModelValue = ref(props.modelValue);
245
256
  const editingLocation = ref(false);
246
257
  const fullScreen = ref(false);
258
+ const leftOverlayHeight = ref<number>();
259
+ const leftOverlayWidth = ref<number>();
260
+ const resizeObserver = ref<ResizeObserver | null>(null);
247
261
 
248
262
  const mapId = `map-${Math.random().toString(36).substring(7)}`;
249
263
  const defaultZoom = 15;
@@ -256,52 +270,52 @@ export default defineComponent({
256
270
  let map: L.Map;
257
271
  let markerLayerGroup: L.FeatureGroup | any;
258
272
 
259
- if (props.editable) {
260
- markerLayerGroup = new LL.FeatureGroup();
261
- }
262
- else {
263
- markerLayerGroup = new LL.MarkerClusterGroup({
264
- spiderfyOnMaxZoom: false,
265
- showCoverageOnHover: false,
266
- disableClusteringAtZoom: 17,
267
- iconCreateFunction: function (cluster: any) {
268
- return LL.divIcon({
269
- html: `<div>
270
- <span>${cluster.getChildCount()}</span>
271
- </div>`,
272
- className: 'fs-map-location fs-map-location-full',
273
- iconSize: [36, 36],
274
- iconAnchor: [18, 18],
275
- });
276
- }
277
- });
278
- }
279
273
  const mapLayers: MapLayer[] = [
280
274
  {
281
- name: "osm",
282
- label: $tr("ui.map.layer.osm", "Map"),
283
- image: new URL("../../assets/images/map/osm.png", import.meta.url).href,
284
- layer: LL.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
285
- maxZoom: 20,
286
- attribution: '© OpenStreetMap'
275
+ name: "map",
276
+ label: $tr("ui.map.layer.map", "Map"),
277
+ image: new URL("../../assets/images/map/map.png", import.meta.url).href,
278
+ layer: LL.tileLayer(`http://{s}.google.com/vt/lyrs=m&x={x}&y={y}&z={z}&key=${import.meta.env.VITE_GOOGLE_MAPS_API_KEY ?? ""}`, {
279
+ maxZoom: 22,
280
+ subdomains: ['mt0', 'mt1', 'mt2', 'mt3'],
281
+ attribution: '© Google Map Data'
287
282
  })
288
283
  },
289
284
  {
290
285
  name: "imagery",
291
286
  label: $tr("ui.map.layer.imagery", "Imagery"),
292
287
  image: new URL("../../assets/images/map/imagery.png", import.meta.url).href,
293
- layer: LL.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
294
- attribution: 'Tiles &copy; Esri &mdash; Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community',
295
- maxZoom: 19
288
+ layer: LL.tileLayer(`http://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}&key=${import.meta.env.VITE_GOOGLE_MAPS_API_KEY ?? ""}`, {
289
+ maxZoom: 22,
290
+ subdomains: ['mt0', 'mt1', 'mt2', 'mt3'],
291
+ attribution: '© Google Map Data'
296
292
  })
297
293
  }
298
294
  ];
299
295
 
296
+ const bottomMargin = computed(() => {
297
+ let margin = 0;
298
+ if (props.overlayMode !== 'expand' && leftOverlayHeight.value && isExtraSmall.value) {
299
+ margin += leftOverlayHeight.value;
300
+ }
301
+ return margin;
302
+ });
303
+
304
+ const leftMargin = computed(() => {
305
+ let margin = 0;
306
+ if (leftOverlayWidth.value && !isExtraSmall.value) {
307
+ margin += leftOverlayWidth.value;
308
+ }
309
+ return margin;
310
+ });
311
+
300
312
  const style = computed((): { [key: string]: string | undefined } => {
301
313
  return {
302
314
  "--fs-map-location-pin-color": getColors(ColorEnum.Primary).base,
315
+ "--fs-map-mylocation-pin-color": getColors(ColorEnum.Primary).base,
303
316
  "--fs-map-mylocation-pin-color-alpha": getColors(ColorEnum.Primary).base + "50",
304
317
  "--fs-map-leaflet-container-height": props.height as string,
318
+ "--fs-map-leaflet-bottom-overlay-margin": `${bottomMargin.value}px`,
305
319
  "--fs-map-container-grayscale": props.grayscale ? '0.9' : '0'
306
320
  };
307
321
  });
@@ -309,18 +323,7 @@ export default defineComponent({
309
323
  const displayLocations = () => {
310
324
  markerLayerGroup.clearLayers();
311
325
  innerModelValue.value.forEach((location) => {
312
- const iconHtml = `
313
- <div style="--fs-map-mylocation-pin-color-alpha:${getColors(location.color).base}50;--fs-map-location-pin-color: ${getColors(location.color).base}">
314
- <div class="fs-map-location-pin">
315
- <i class="${location.icon} mdi v-icon notranslate v-theme--DefaultTheme fs-icon" aria-hidden="true" style="--fs-icon-font-size: 22px;" ></i>
316
- </div>
317
- </div>`;
318
- const icon = LL.divIcon({
319
- html: iconHtml,
320
- iconSize: [36, 36],
321
- className: 'fs-map-location',
322
- iconAnchor: [18, 18],
323
- });
326
+ const icon = locationMarker(location.icon, getColors(location.color).base, L);
324
327
  const marker = LL.marker([location.address.latitude, location.address.longitude], { icon }).addTo(markerLayerGroup);
325
328
  markers[location.id] = marker;
326
329
  marker.on('click', () => emit('update:selectedLocationId', location.id));
@@ -357,9 +360,22 @@ export default defineComponent({
357
360
  };
358
361
 
359
362
  const initMap = () => {
363
+ if (props.editable) {
364
+ markerLayerGroup = new LL.FeatureGroup();
365
+ }
366
+ else {
367
+ markerLayerGroup = new LL.MarkerClusterGroup({
368
+ spiderfyOnMaxZoom: false,
369
+ showCoverageOnHover: false,
370
+ disableClusteringAtZoom: 17,
371
+ iconCreateFunction: function (cluster: any) {
372
+ return clusterMarker(cluster.getChildCount(), L);
373
+ }
374
+ });
375
+ }
360
376
  const mapOptions = {
361
377
  zoomControl: false,
362
- scrollWheelZoom: false,
378
+ scrollWheelZoom: props.enableScrollWheelZoom,
363
379
  minZoom: 2,
364
380
  maxBounds: LL.latLngBounds(LL.latLng(-90, -180), LL.latLng(90, 180)),
365
381
  maxBoundsViscosity: 1.0
@@ -387,7 +403,7 @@ export default defineComponent({
387
403
  });
388
404
  };
389
405
 
390
- const setMapBaseLayer = (layerName: 'osm' | 'imagery') => {
406
+ const setMapBaseLayer = (layerName: 'map' | 'imagery') => {
391
407
  const layer = mapLayers.find((mapLayer) => mapLayer.name === layerName) ?? mapLayers[0];
392
408
  baseLayerGroup.clearLayers();
393
409
  layer.layer.addTo(baseLayerGroup);
@@ -398,7 +414,7 @@ export default defineComponent({
398
414
  return;
399
415
  }
400
416
  modifyLocationAddress(props.selectedLocationId, address);
401
- map.panTo([address.latitude, address.longitude]);
417
+ map.panTo(calculateTargetPosition(new L.LatLng(address.latitude, address.longitude)));
402
418
  };
403
419
 
404
420
  const onNewCoordEntered = async (lat: number, lng: number) => {
@@ -431,19 +447,18 @@ export default defineComponent({
431
447
  }
432
448
  map.locate();
433
449
  map.on('locationfound', (e: L.LocationEvent) => {
434
- map.panTo(e.latlng);
435
- const iconHtml = `<div class="fs-map-mylocation-pin"></div>`;
436
- const icon = LL.divIcon({
437
- html: iconHtml,
438
- className: 'fs-map-mylocation',
439
- iconSize: [16, 16],
440
- iconAnchor: [8, 8],
441
- });
450
+ map.panTo(calculateTargetPosition(e.latlng));
451
+ const icon = myLocationMarker(L);
442
452
  myLocationLayerGroup.clearLayers();
443
453
  LL.marker(e.latlng, { icon }).addTo(myLocationLayerGroup);
444
454
  });
445
455
  };
446
456
 
457
+ const calculateTargetPosition = (target: L.LatLng, zoom: number = map.getZoom()) => {
458
+ const targetPoint = map.project(target, zoom).subtract([leftMargin.value / 2, -bottomMargin.value / 2]);
459
+ return map.unproject(targetPoint, zoom);
460
+ }
461
+
447
462
  const onCancel = () => {
448
463
  editingLocation.value = false;
449
464
  innerModelValue.value = props.modelValue;
@@ -455,7 +470,7 @@ export default defineComponent({
455
470
  map.fitBounds(markerLayerGroup.getBounds(), { maxZoom: defaultZoom });
456
471
  }
457
472
  else {
458
- map.panTo([props.center[0], props.center[1]]);
473
+ map.panTo(calculateTargetPosition(new L.LatLng(props.center[0], props.center[1])), { animate: false });
459
474
  }
460
475
  if (props.modelValue.length > 1) {
461
476
  emit('update:selectedLocationId', null);
@@ -472,7 +487,7 @@ export default defineComponent({
472
487
  map.fitBounds(markerLayerGroup.getBounds(), { maxZoom: defaultZoom });
473
488
  }
474
489
  else {
475
- map.flyTo([props.center[0], props.center[1]], map.getZoom() ?? defaultZoom, { animate: false });
490
+ map.panTo(calculateTargetPosition(new L.LatLng(props.center[0], props.center[1])), { animate: false });
476
491
  }
477
492
  if (props.modelValue.length > 1) {
478
493
  emit('update:selectedLocationId', null);
@@ -484,6 +499,29 @@ export default defineComponent({
484
499
  if (props.selectedLocationId && props.modelValue.length === 1) {
485
500
  editingLocation.value = true;
486
501
  }
502
+
503
+ resizeObserver.value = new ResizeObserver(entries => {
504
+ entries.forEach((entry) => {
505
+ if (entry.target.id === `left-overlay-${mapId}`) {
506
+ leftOverlayWidth.value = entry.contentRect.width;
507
+ }
508
+ if(entry.target.id === `left-overlay-mobile-${mapId}`) {
509
+ leftOverlayHeight.value = entry.contentRect.height;
510
+ }
511
+ });
512
+ });
513
+ if (document.querySelector(`#left-overlay-mobile-${mapId}`)) {
514
+ resizeObserver.value.observe(document.querySelector(`#left-overlay-mobile-${mapId}`)!);
515
+ }
516
+ if (document.querySelector(`#left-overlay-${mapId}`)) {
517
+ resizeObserver.value.observe(document.querySelector(`#left-overlay-${mapId}`)!);
518
+ }
519
+ });
520
+
521
+ onUnmounted((): void => {
522
+ if (resizeObserver.value) {
523
+ resizeObserver.value.disconnect();
524
+ }
487
525
  });
488
526
 
489
527
  watch(() => innerModelValue.value, () => {
@@ -500,7 +538,7 @@ export default defineComponent({
500
538
  });
501
539
 
502
540
  const marker = markers[props.selectedLocationId];
503
- map.flyTo(marker.getLatLng(), 17, { animate: false });
541
+ map.flyTo(calculateTargetPosition(marker.getLatLng(), 17), 17, { animate: false });
504
542
  marker.getElement()?.classList.add('fs-map-location-selected');
505
543
  })
506
544
 
@@ -519,10 +557,11 @@ export default defineComponent({
519
557
  });
520
558
 
521
559
  return {
522
- innerSelectedLayer,
560
+ bottomMargin,
523
561
  editingLocation,
524
- innerModelValue,
525
562
  fullScreen,
563
+ innerModelValue,
564
+ innerSelectedLayer,
526
565
  mapLayers,
527
566
  mapId,
528
567
  style,
@@ -536,6 +575,6 @@ export default defineComponent({
536
575
  locate,
537
576
  zoomIn
538
577
  };
539
- },
578
+ }
540
579
  });
541
580
  </script>
@@ -7,20 +7,24 @@
7
7
  <FSDialog
8
8
  v-model="dialog"
9
9
  title="Select Layers"
10
- width="460px"
10
+ width="442px"
11
11
  >
12
12
  <template
13
13
  v-slot:body
14
14
  >
15
- <FSImageCard
16
- v-for="layer in layers"
17
- :variant="modelValue === layer.name ? 'full' : 'background'"
18
- :color="modelValue === layer.name ? 'primary' : 'light'"
19
- :label="layer.label"
20
- :src="layer.image"
21
- :key="layer.name"
22
- @click="onLayerClick(layer.name)"
23
- />
15
+ <FSRow
16
+ align="center-center"
17
+ >
18
+ <FSImageCard
19
+ v-for="layer in layers"
20
+ :variant="modelValue === layer.name ? 'full' : 'background'"
21
+ :color="modelValue === layer.name ? 'primary' : 'light'"
22
+ :label="layer.label"
23
+ :src="layer.image"
24
+ :key="layer.name"
25
+ @click="onLayerClick(layer.name)"
26
+ />
27
+ </FSRow>
24
28
  </template>
25
29
  </FSDialog>
26
30
  </template>
@@ -33,13 +37,15 @@ import { type MapLayer } from "../../models";
33
37
  import FSImageCard from "../FSImageCard.vue";
34
38
  import FSButton from "../FSButton.vue";
35
39
  import FSDialog from "../FSDialog.vue";
40
+ import FSRow from "../FSRow.vue";
36
41
 
37
42
  export default defineComponent({
38
43
  name: "FSMapLayerButton",
39
44
  components: {
40
45
  FSImageCard,
41
46
  FSButton,
42
- FSDialog
47
+ FSDialog,
48
+ FSRow
43
49
  },
44
50
  props: {
45
51
  layers: {