@saasmakers/ui 0.1.107 → 0.1.108

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 (33) hide show
  1. package/app/components/bases/BaseAlert.vue +13 -10
  2. package/app/components/bases/BaseAvatar.vue +27 -2
  3. package/app/components/bases/BaseCharacter.vue +8 -7
  4. package/app/components/bases/BaseChart.vue +21 -19
  5. package/app/components/bases/BaseEmoji.vue +11 -2
  6. package/app/components/bases/BaseHeading.stories.ts +1 -1
  7. package/app/components/bases/BaseHeading.vue +7 -1
  8. package/app/components/bases/BaseIcon.vue +41 -25
  9. package/app/components/bases/BaseMessage.vue +4 -6
  10. package/app/components/bases/BaseMetric.vue +9 -8
  11. package/app/components/bases/BaseOverlay.vue +17 -3
  12. package/app/components/bases/BaseParagraph.stories.ts +1 -1
  13. package/app/components/bases/BaseParagraph.vue +7 -1
  14. package/app/components/bases/BaseQuote.stories.ts +1 -1
  15. package/app/components/bases/BaseQuote.vue +19 -17
  16. package/app/components/bases/BaseSpinner.vue +10 -1
  17. package/app/components/bases/BaseTag.vue +21 -1
  18. package/app/components/bases/BaseTags.vue +3 -4
  19. package/app/components/bases/BaseToast.vue +10 -1
  20. package/app/components/fields/FieldDays.vue +2 -1
  21. package/app/components/fields/FieldEmojis.vue +3 -3
  22. package/app/components/fields/FieldLabel.vue +24 -8
  23. package/app/components/fields/FieldMessage.vue +60 -92
  24. package/app/components/fields/FieldSelect.vue +33 -8
  25. package/app/components/fields/FieldTime.vue +12 -1
  26. package/app/composables/useChartist.ts +1 -1
  27. package/app/composables/useDevice.ts +1 -0
  28. package/app/composables/useLayerUtils.ts +16 -16
  29. package/app/composables/useToasts.ts +1 -1
  30. package/app/composables/useTranslation.ts +1 -1
  31. package/app/types/bases.d.ts +2 -0
  32. package/nuxt.config.ts +6 -6
  33. package/package.json +17 -13
@@ -25,17 +25,20 @@ const { getIcon } = useLayerIcons()
25
25
  const isClosed = ref(false)
26
26
 
27
27
  const buttonColor = computed<BaseColor>(() => {
28
- if (props.status === 'error') {
29
- return 'red'
28
+ switch (props.status) {
29
+ case 'error': {
30
+ return 'red'
31
+ }
32
+ case 'success': {
33
+ return 'green'
34
+ }
35
+ case 'warning': {
36
+ return 'orange'
37
+ }
38
+ default: {
39
+ return 'indigo'
40
+ }
30
41
  }
31
- else if (props.status === 'success') {
32
- return 'green'
33
- }
34
- else if (props.status === 'warning') {
35
- return 'orange'
36
- }
37
-
38
- return 'indigo'
39
42
  })
40
43
 
41
44
  onBeforeMount(async () => {
@@ -31,6 +31,10 @@ const loaded = ref(false)
31
31
 
32
32
  const { t } = useI18n()
33
33
 
34
+ const isClickable = computed(() => {
35
+ return props.to || props.editable
36
+ })
37
+
34
38
  function onAvatarSelected(event: Event) {
35
39
  const file = (event.target as HTMLInputElement).files?.[0]
36
40
 
@@ -43,6 +47,10 @@ function onAvatarSelected(event: Event) {
43
47
  }
44
48
  }
45
49
 
50
+ function onBlur() {
51
+ hovered.value = false
52
+ }
53
+
46
54
  function onClick(event: MouseEvent) {
47
55
  emit('click', event)
48
56
 
@@ -56,6 +64,16 @@ function onError() {
56
64
  loaded.value = true
57
65
  }
58
66
 
67
+ function onFocus() {
68
+ hovered.value = true
69
+ }
70
+
71
+ function onKeydown(event: KeyboardEvent) {
72
+ if ((event.key === 'Enter' || event.key === ' ') && isClickable.value) {
73
+ onClick(event as unknown as MouseEvent)
74
+ }
75
+ }
76
+
59
77
  function onLoad() {
60
78
  loaded.value = true
61
79
  }
@@ -76,6 +94,8 @@ function onMouseLeave() {
76
94
  :to="to"
77
95
  @mouseenter="onMouseEnter"
78
96
  @mouseleave="onMouseLeave"
97
+ @focusin="onFocus"
98
+ @focusout="onBlur"
79
99
  >
80
100
  <div
81
101
  :aria-label="editable ? t('edit') : undefined"
@@ -83,10 +103,13 @@ function onMouseLeave() {
83
103
  :class="{
84
104
  'rounded-full': circular,
85
105
  'shadow': shadow,
86
- 'cursor-pointer': to || editable,
106
+ 'cursor-pointer': isClickable,
87
107
  }"
88
- :role="editable ? 'button' : undefined"
108
+ role="button"
109
+ :aria-disabled="!isClickable"
110
+ tabindex="0"
89
111
  @click="onClick"
112
+ @keydown.prevent="onKeydown"
90
113
  >
91
114
  <img
92
115
  class="h-full w-full object-cover drag-none"
@@ -97,6 +120,7 @@ function onMouseLeave() {
97
120
  'border-3': borderWidth === 3,
98
121
  'border-4': borderWidth === 4,
99
122
  }"
123
+ :alt="editable ? t('edit') : ''"
100
124
  loading="lazy"
101
125
  :src="!error && src ? src : '/images/bases/BaseAvatar/default.svg'"
102
126
  @error="onError"
@@ -114,6 +138,7 @@ function onMouseLeave() {
114
138
  v-if="editable"
115
139
  ref="fileInput"
116
140
  accept="image/*"
141
+ :aria-label="t('edit')"
117
142
  class="hidden"
118
143
  type="file"
119
144
  @change="onAvatarSelected"
@@ -15,17 +15,18 @@ const width = computed(() => {
15
15
  }
16
16
 
17
17
  switch (props.character) {
18
- case 'nada':
18
+ case 'nada': {
19
19
  return size[props.size] + 4
20
-
21
- case 'power':
20
+ }
21
+ case 'power': {
22
22
  return size[props.size] + 3
23
-
24
- case 'yoda':
23
+ }
24
+ case 'yoda': {
25
25
  return size[props.size] + 8
26
-
27
- default:
26
+ }
27
+ default: {
28
28
  return size[props.size]
29
+ }
29
30
  }
30
31
  })
31
32
  </script>
@@ -2,7 +2,7 @@
2
2
  import { BarChart, LineChart, PieChart } from 'chartist'
3
3
  import type { BaseChart } from '../../types/bases'
4
4
 
5
- const props = withDefaults(defineProps<BaseChart>(), {
5
+ const properties = withDefaults(defineProps<BaseChart>(), {
6
6
  data: () => ({
7
7
  labels: [],
8
8
  series: [],
@@ -16,18 +16,12 @@ const chart = ref()
16
16
  const root = ref<HTMLDivElement>()
17
17
 
18
18
  watch(
19
- () => props.data,
19
+ () => properties.data,
20
20
  () => {
21
21
  if (chart.value) {
22
- if (props.type === 'line') {
23
- chart.value.update((props.data as ChartistLineChartData), props.options)
24
- }
25
- else if (props.type === 'bar') {
26
- chart.value.update((props.data as ChartistBarChartData), props.options)
27
- }
28
- else if (props.type === 'pie') {
29
- chart.value.update((props.data as ChartistPieChartData), props.options)
30
- }
22
+ const data = properties.data
23
+
24
+ return chart.value.update(data, properties.options)
31
25
  }
32
26
  },
33
27
  { deep: true },
@@ -35,14 +29,22 @@ watch(
35
29
 
36
30
  onMounted(() => {
37
31
  if (root.value) {
38
- if (props.type === 'line') {
39
- chart.value = new LineChart(root.value, (props.data as ChartistLineChartData), props.options)
40
- }
41
- else if (props.type === 'bar') {
42
- chart.value = new BarChart(root.value, (props.data as ChartistBarChartData), props.options)
43
- }
44
- else if (props.type === 'pie') {
45
- chart.value = new PieChart(root.value, (props.data as ChartistPieChartData), props.options)
32
+ switch (properties.type) {
33
+ case 'bar': {
34
+ chart.value = new BarChart(root.value, (properties.data as ChartistBarChartData), properties.options)
35
+
36
+ break
37
+ }
38
+ case 'line': {
39
+ chart.value = new LineChart(root.value, (properties.data as ChartistLineChartData), properties.options)
40
+
41
+ break
42
+ }
43
+ case 'pie': {
44
+ chart.value = new PieChart(root.value, (properties.data as ChartistPieChartData), properties.options)
45
+
46
+ break
47
+ }
46
48
  }
47
49
  }
48
50
  })
@@ -21,6 +21,12 @@ const { urls } = useAppConfig()
21
21
  function onClick(event: MouseEvent) {
22
22
  emit('click', event, props.emoji)
23
23
  }
24
+
25
+ function onKeydown(event: KeyboardEvent) {
26
+ if ((event.key === 'Enter' || event.key === ' ') && props.clickable) {
27
+ onClick(event as unknown as MouseEvent)
28
+ }
29
+ }
24
30
  </script>
25
31
 
26
32
  <template>
@@ -43,11 +49,14 @@ function onClick(event: MouseEvent) {
43
49
  'h-16 w-16': size === '3xl' && hasBox,
44
50
  'h-18 w-18': size === '4xl' && hasBox,
45
51
  }"
52
+ role="button"
53
+ tabindex="0"
46
54
  @click="onClick"
55
+ @keydown.prevent="onKeydown"
47
56
  >
48
57
  <img
49
58
  v-if="emoji"
50
- class="flex"
59
+ class="pointer-events-none flex"
51
60
  :class="{
52
61
  'h-2.5 w-2.5': size === '3xs',
53
62
  'h-3 w-3': size === '2xs',
@@ -60,9 +69,9 @@ function onClick(event: MouseEvent) {
60
69
  'h-10 w-10': size === '3xl',
61
70
  'h-12 w-12': size === '4xl',
62
71
  }"
72
+ :alt="emoji"
63
73
  loading="lazy"
64
74
  :src="`${urls.storage}/images/emojis/${emoji}.svg`"
65
- @click.stop="onClick"
66
75
  >
67
76
 
68
77
  <slot />
@@ -39,7 +39,7 @@ export default {
39
39
 
40
40
  export const Default: StoryObj<typeof BaseHeading> = {
41
41
  args: { tag: 'h1' } satisfies Partial<BaseHeading>,
42
- render: args => ({
42
+ render: (args) => ({
43
43
  components: { BaseHeading },
44
44
  setup() {
45
45
  return { args }
@@ -5,6 +5,7 @@ withDefaults(defineProps<BaseHeading>(), {
5
5
  alignment: 'left',
6
6
  size: 'base',
7
7
  tag: 'h1',
8
+ text: '',
8
9
  })
9
10
 
10
11
  defineSlots<{
@@ -27,6 +28,11 @@ defineSlots<{
27
28
  'text-2xl sm:text-4xl': size === 'lg',
28
29
  }"
29
30
  >
30
- <slot />
31
+ <BaseText
32
+ v-if="text"
33
+ :text="text"
34
+ />
35
+
36
+ <slot v-else />
31
37
  </component>
32
38
  </template>
@@ -29,40 +29,47 @@ const confirming = ref(false)
29
29
  const { t } = useI18n()
30
30
  const { getIcon } = useLayerIcons()
31
31
 
32
+ const isClickable = computed(() => {
33
+ return props.clickable || props.to
34
+ })
35
+
32
36
  const statusIcon = computed<string | undefined>(() => {
33
37
  switch (props.status) {
34
- case 'error':
38
+ case 'error': {
35
39
  return getIcon('closeCircle')
36
-
37
- case 'info':
40
+ }
41
+ case 'info': {
38
42
  return getIcon('infoCircle')
39
-
40
- case 'success':
43
+ }
44
+ case 'success': {
41
45
  return getIcon('checkCircle')
42
-
43
- case 'warning':
46
+ }
47
+ case 'warning': {
44
48
  return getIcon('exclamationCircle')
45
-
46
- default:
47
- return undefined
49
+ }
50
+ default: {
51
+ return
52
+ }
48
53
  }
49
54
  })
50
55
 
51
56
  const statusColor = computed<BaseColor | undefined>(() => {
52
- if (props.status === 'error') {
53
- return 'red'
54
- }
55
- else if (props.status === 'info') {
56
- return 'indigo'
57
- }
58
- else if (props.status === 'success') {
59
- return 'green'
60
- }
61
- else if (props.status === 'warning') {
62
- return 'orange'
63
- }
64
- else {
65
- return undefined
57
+ switch (props.status) {
58
+ case 'error': {
59
+ return 'red'
60
+ }
61
+ case 'info': {
62
+ return 'indigo'
63
+ }
64
+ case 'success': {
65
+ return 'green'
66
+ }
67
+ case 'warning': {
68
+ return 'orange'
69
+ }
70
+ default: {
71
+ return
72
+ }
66
73
  }
67
74
  })
68
75
 
@@ -91,17 +98,26 @@ function onClick(event: MouseEvent) {
91
98
 
92
99
  emit('click', event)
93
100
  }
101
+
102
+ function onKeydown(event: KeyboardEvent) {
103
+ if ((event.key === 'Enter' || event.key === ' ') && isClickable.value) {
104
+ onClick(event as unknown as MouseEvent)
105
+ }
106
+ }
94
107
  </script>
95
108
 
96
109
  <template>
97
110
  <div
98
111
  class="flex items-center"
99
112
  :class="{
100
- 'cursor-pointer hover:underline hover:text-gray-900 dark:hover:text-gray-100': clickable || to,
113
+ 'cursor-pointer hover:underline hover:text-gray-900 dark:hover:text-gray-100': isClickable,
101
114
  'flex-row': !reverse,
102
115
  'flex-row-reverse': reverse,
103
116
  }"
117
+ role="button"
118
+ tabindex="0"
104
119
  @click="onClick"
120
+ @keydown.prevent="onKeydown"
105
121
  >
106
122
  <component
107
123
  :is="to ? NuxtLinkLocale : 'span'"
@@ -32,17 +32,15 @@ defineSlots<{
32
32
  alignment="center"
33
33
  size="sm"
34
34
  tag="h1"
35
- >
36
- <span v-html="title" />
37
- </BaseHeading>
35
+ :text="title"
36
+ />
38
37
 
39
38
  <BaseParagraph
40
39
  v-if="description"
41
40
  alignment="center"
42
41
  class="mt-2"
43
- >
44
- <span v-html="description" />
45
- </BaseParagraph>
42
+ :text="description"
43
+ />
46
44
 
47
45
  <div
48
46
  v-if="$slots.default"
@@ -14,17 +14,18 @@ const { getIcon } = useLayerIcons()
14
14
 
15
15
  const performanceIcon = computed<string | undefined>(() => {
16
16
  switch (props.performance) {
17
- case 'down':
17
+ case 'down': {
18
18
  return getIcon('arrowDown')
19
-
20
- case 'equal':
19
+ }
20
+ case 'equal': {
21
21
  return getIcon('arrowRight')
22
-
23
- case 'up':
22
+ }
23
+ case 'up': {
24
24
  return getIcon('arrowUp')
25
-
26
- default:
27
- return undefined
25
+ }
26
+ default: {
27
+ return
28
+ }
28
29
  }
29
30
  })
30
31
  </script>
@@ -4,7 +4,7 @@ import { Motion } from 'motion-v'
4
4
  import type { BaseOverlay } from '../../types/bases'
5
5
  import useMotion from '../../composables/useMotion'
6
6
 
7
- withDefaults(defineProps<BaseOverlay>(), {
7
+ const props = withDefaults(defineProps<BaseOverlay>(), {
8
8
  active: true,
9
9
  clickable: true,
10
10
  fixed: true,
@@ -25,6 +25,10 @@ defineSlots<{
25
25
  const { getIcon } = useLayerIcons()
26
26
  const { fadeIn } = useMotion()
27
27
 
28
+ const isClickable = computed(() => {
29
+ return props.clickable || props.hasClose
30
+ })
31
+
28
32
  function onClick(event: MouseEvent) {
29
33
  emit('click', event)
30
34
  }
@@ -33,6 +37,12 @@ function onClose(event: KeyboardEvent | MouseEvent) {
33
37
  emit('close', event)
34
38
  }
35
39
 
40
+ function onKeydown(event: KeyboardEvent) {
41
+ if ((event.key === 'Enter' || event.key === ' ') && isClickable.value) {
42
+ onClick(event as unknown as MouseEvent)
43
+ }
44
+ }
45
+
36
46
  onKeyStroke('Escape', (event) => {
37
47
  event.preventDefault()
38
48
 
@@ -44,13 +54,17 @@ onKeyStroke('Escape', (event) => {
44
54
  <div
45
55
  :class="{
46
56
  'fixed inset-0': fixed,
47
- 'cursor-pointer': clickable || hasClose,
57
+ 'cursor-pointer': isClickable,
48
58
  }"
59
+ role="button"
60
+ tabindex="0"
49
61
  @click="onClick"
62
+ @keydown.prevent="onKeydown"
50
63
  >
51
64
  <BaseIcon
52
65
  v-if="hasClose"
53
- class="absolute right-4 top-4 z-50 text-gray-200 dark:text-gray-800"
66
+ class="pointer-events-auto absolute right-4 top-4 z-50 text-gray-200 dark:text-gray-800"
67
+ clickable
54
68
  :icon="getIcon('close')"
55
69
  @click="onClose"
56
70
  />
@@ -26,7 +26,7 @@ export default {
26
26
 
27
27
  export const Default: StoryObj<typeof BaseParagraph> = {
28
28
  args: {},
29
- render: args => ({
29
+ render: (args) => ({
30
30
  components: { BaseParagraph },
31
31
  setup() {
32
32
  return { args }
@@ -4,6 +4,7 @@ import type { BaseParagraph } from '../../types/bases'
4
4
  withDefaults(defineProps<BaseParagraph>(), {
5
5
  alignment: 'left',
6
6
  size: 'base',
7
+ text: '',
7
8
  })
8
9
 
9
10
  defineSlots<{
@@ -23,6 +24,11 @@ defineSlots<{
23
24
  'text-lg sm:text-xl': size === 'lg',
24
25
  }"
25
26
  >
26
- <slot />
27
+ <BaseText
28
+ v-if="text"
29
+ :text="text"
30
+ />
31
+
32
+ <slot v-else />
27
33
  </p>
28
34
  </template>
@@ -53,7 +53,7 @@ export default {
53
53
 
54
54
  export const Default: StoryObj<typeof BaseQuote> = {
55
55
  args: {},
56
- render: args => ({
56
+ render: (args) => ({
57
57
  components: { BaseQuote },
58
58
  setup() {
59
59
  return { args }
@@ -34,31 +34,30 @@ const { getIcon } = useLayerIcons()
34
34
  const closed = ref(false)
35
35
 
36
36
  const finalBackground = computed(() => {
37
- if ((props.reverse && props.background !== 'gray-light') || props.background === 'gray') {
38
- return 'gray'
39
- }
40
- else {
41
- return props.background
42
- }
37
+ const isBackgroundGray = props.background === 'gray'
38
+ const isReverseWithNonGrayLight = props.reverse && props.background !== 'gray-light'
39
+
40
+ return (isBackgroundGray || isReverseWithNonGrayLight) ? 'gray' : props.background
43
41
  })
44
42
 
45
43
  const finalReverse = computed(() => {
46
- return props.reverse !== null ? props.reverse : props.character !== 'julien'
44
+ return props.reverse === null ? props.character !== 'julien' : props.reverse
47
45
  })
48
46
 
49
47
  onMounted(() => {
50
- if (props.hasClose && props.closingId) {
51
- closed.value = localStorage.getItem(props.closingId) === 'true'
52
- }
53
- else {
54
- closed.value = false
55
- }
48
+ closed.value = props.hasClose && props.closingId ? localStorage.getItem(props.closingId) === 'true' : false
56
49
  })
57
50
 
58
51
  function onBubbleClick(event: MouseEvent) {
59
52
  emit('click', event)
60
53
  }
61
54
 
55
+ function onBubbleKeydown(event: KeyboardEvent) {
56
+ if ((event.key === 'Enter' || event.key === ' ') && props.clickable) {
57
+ onBubbleClick(event as unknown as MouseEvent)
58
+ }
59
+ }
60
+
62
61
  function onClose(event: MouseEvent) {
63
62
  if (props.closingId) {
64
63
  localStorage.setItem(props.closingId, 'true')
@@ -108,7 +107,11 @@ function onClose(event: MouseEvent) {
108
107
  'border-green-600 dark:border-green-400 text-green-600 dark:text-green-400': status === 'success',
109
108
  'border-orange-600 dark:border-orange-400 text-orange-600 dark:text-orange-400': status === 'warning',
110
109
  }"
110
+ role="button"
111
+ :aria-disabled="!clickable"
112
+ tabindex="0"
111
113
  @click="onBubbleClick"
114
+ @keydown.prevent="onBubbleKeydown"
112
115
  >
113
116
  <div class="flex items-center">
114
117
  <div
@@ -119,10 +122,9 @@ function onClose(event: MouseEvent) {
119
122
  </div>
120
123
 
121
124
  <div class="flex-1">
122
- <p
123
- v-if="!forceSlot"
124
- v-html="translatedContent"
125
- />
125
+ <p v-if="!forceSlot">
126
+ {{ translatedContent }}
127
+ </p>
126
128
 
127
129
  <slot v-else />
128
130
  </div>
@@ -18,6 +18,12 @@ const emit = defineEmits<{
18
18
  function onClick(event: MouseEvent) {
19
19
  emit('click', event, props.id)
20
20
  }
21
+
22
+ function onKeydown(event: KeyboardEvent) {
23
+ if ((event.key === 'Enter' || event.key === ' ') && props.clickable) {
24
+ onClick(event as unknown as MouseEvent)
25
+ }
26
+ }
21
27
  </script>
22
28
 
23
29
  <template>
@@ -28,7 +34,11 @@ function onClick(event: MouseEvent) {
28
34
  'flex-row': !reverse,
29
35
  'flex-row-reverse': reverse,
30
36
  }"
37
+ role="button"
38
+ :aria-disabled="!clickable"
39
+ tabindex="0"
31
40
  @click="onClick"
41
+ @keydown.prevent="onKeydown"
32
42
  >
33
43
  <div
34
44
  class="relative inline-block cursor-wait flex-initial"
@@ -44,7 +54,6 @@ function onClick(event: MouseEvent) {
44
54
  'h-10 w-10': size === '3xl',
45
55
  'h-12 w-12': size === '4xl',
46
56
  }"
47
- @click="onClick"
48
57
  >
49
58
  <div
50
59
  v-for="wave in 2"
@@ -49,7 +49,11 @@ onBeforeMount(() => {
49
49
  })
50
50
 
51
51
  function initializeFormForExistingTag() {
52
- form.name = String(props.text)?.substring(1)
52
+ form.name = String(props.text)?.slice(1)
53
+ }
54
+
55
+ function onBlur() {
56
+ hovered.value = false
53
57
  }
54
58
 
55
59
  function onClick(event: MouseEvent) {
@@ -58,6 +62,10 @@ function onClick(event: MouseEvent) {
58
62
  }
59
63
  }
60
64
 
65
+ function onFocus() {
66
+ hovered.value = true
67
+ }
68
+
61
69
  function onInputBlur(event: FocusEvent) {
62
70
  emit('inputBlur', event, form.name.trim(), props.id)
63
71
  }
@@ -66,6 +74,12 @@ function onInputSubmit(event: KeyboardEvent) {
66
74
  emit('inputSubmit', event, form.name.trim(), props.id)
67
75
  }
68
76
 
77
+ function onKeydown(event: KeyboardEvent) {
78
+ if ((event.key === 'Enter' || event.key === ' ') && props.clickable) {
79
+ onClick(event as unknown as MouseEvent)
80
+ }
81
+ }
82
+
69
83
  function onMouseEnter() {
70
84
  hovered.value = true
71
85
  }
@@ -82,9 +96,15 @@ function onRemove(event: MouseEvent) {
82
96
  <template>
83
97
  <span
84
98
  class="flex select-none"
99
+ role="button"
100
+ :aria-disabled="!clickable"
101
+ tabindex="0"
85
102
  @click.stop="onClick"
103
+ @keydown.prevent="onKeydown"
86
104
  @mouseenter="onMouseEnter"
87
105
  @mouseleave="onMouseLeave"
106
+ @focusin="onFocus"
107
+ @focusout="onBlur"
88
108
  >
89
109
  <component
90
110
  :is="to ? NuxtLinkLocale : 'span'"