@saasmakers/ui 0.1.107 → 0.1.109

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 +28 -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 +12 -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 +42 -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 +18 -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 +20 -17
  16. package/app/components/bases/BaseSpinner.vue +11 -1
  17. package/app/components/bases/BaseTag.vue +22 -1
  18. package/app/components/bases/BaseTags.vue +3 -4
  19. package/app/components/bases/BaseToast.vue +7 -3
  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 +20 -7
  23. package/app/components/fields/FieldMessage.vue +60 -92
  24. package/app/components/fields/FieldSelect.vue +34 -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 (isClickable.value) {
73
+ (event.currentTarget as HTMLElement).click()
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,14 @@ 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.enter="onKeyDown"
113
+ @keydown.space.prevent="onKeyDown"
90
114
  >
91
115
  <img
92
116
  class="h-full w-full object-cover drag-none"
@@ -97,6 +121,7 @@ function onMouseLeave() {
97
121
  'border-3': borderWidth === 3,
98
122
  'border-4': borderWidth === 4,
99
123
  }"
124
+ :alt="editable ? t('edit') : ''"
100
125
  loading="lazy"
101
126
  :src="!error && src ? src : '/images/bases/BaseAvatar/default.svg'"
102
127
  @error="onError"
@@ -114,6 +139,7 @@ function onMouseLeave() {
114
139
  v-if="editable"
115
140
  ref="fileInput"
116
141
  accept="image/*"
142
+ :aria-label="t('edit')"
117
143
  class="hidden"
118
144
  type="file"
119
145
  @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 (props.clickable) {
27
+ (event.currentTarget as HTMLElement).click()
28
+ }
29
+ }
24
30
  </script>
25
31
 
26
32
  <template>
@@ -43,11 +49,15 @@ 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.enter="onKeyDown"
56
+ @keydown.space.prevent="onKeyDown"
47
57
  >
48
58
  <img
49
59
  v-if="emoji"
50
- class="flex"
60
+ class="pointer-events-none flex"
51
61
  :class="{
52
62
  'h-2.5 w-2.5': size === '3xs',
53
63
  'h-3 w-3': size === '2xs',
@@ -60,9 +70,9 @@ function onClick(event: MouseEvent) {
60
70
  'h-10 w-10': size === '3xl',
61
71
  'h-12 w-12': size === '4xl',
62
72
  }"
73
+ :alt="emoji"
63
74
  loading="lazy"
64
75
  :src="`${urls.storage}/images/emojis/${emoji}.svg`"
65
- @click.stop="onClick"
66
76
  >
67
77
 
68
78
  <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,27 @@ function onClick(event: MouseEvent) {
91
98
 
92
99
  emit('click', event)
93
100
  }
101
+
102
+ function onKeyDown(event: KeyboardEvent) {
103
+ if (isClickable.value) {
104
+ (event.currentTarget as HTMLElement).click()
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.enter="onKeyDown"
121
+ @keydown.space.prevent="onKeyDown"
105
122
  >
106
123
  <component
107
124
  :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 (isClickable.value) {
42
+ (event.currentTarget as HTMLElement).click()
43
+ }
44
+ }
45
+
36
46
  onKeyStroke('Escape', (event) => {
37
47
  event.preventDefault()
38
48
 
@@ -44,13 +54,18 @@ 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.enter="onKeyDown"
63
+ @keydown.space.prevent="onKeyDown"
50
64
  >
51
65
  <BaseIcon
52
66
  v-if="hasClose"
53
- class="absolute right-4 top-4 z-50 text-gray-200 dark:text-gray-800"
67
+ class="pointer-events-auto absolute right-4 top-4 z-50 text-gray-200 dark:text-gray-800"
68
+ clickable
54
69
  :icon="getIcon('close')"
55
70
  @click="onClose"
56
71
  />
@@ -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 (props.clickable) {
57
+ (event.currentTarget as HTMLElement).click()
58
+ }
59
+ }
60
+
62
61
  function onClose(event: MouseEvent) {
63
62
  if (props.closingId) {
64
63
  localStorage.setItem(props.closingId, 'true')
@@ -108,7 +107,12 @@ 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.enter="onBubbleKeyDown"
115
+ @keydown.space.prevent="onBubbleKeyDown"
112
116
  >
113
117
  <div class="flex items-center">
114
118
  <div
@@ -119,10 +123,9 @@ function onClose(event: MouseEvent) {
119
123
  </div>
120
124
 
121
125
  <div class="flex-1">
122
- <p
123
- v-if="!forceSlot"
124
- v-html="translatedContent"
125
- />
126
+ <p v-if="!forceSlot">
127
+ {{ translatedContent }}
128
+ </p>
126
129
 
127
130
  <slot v-else />
128
131
  </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 (props.clickable) {
24
+ (event.currentTarget as HTMLElement).click()
25
+ }
26
+ }
21
27
  </script>
22
28
 
23
29
  <template>
@@ -28,7 +34,12 @@ 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.enter="onKeyDown"
42
+ @keydown.space.prevent="onKeyDown"
32
43
  >
33
44
  <div
34
45
  class="relative inline-block cursor-wait flex-initial"
@@ -44,7 +55,6 @@ function onClick(event: MouseEvent) {
44
55
  'h-10 w-10': size === '3xl',
45
56
  'h-12 w-12': size === '4xl',
46
57
  }"
47
- @click="onClick"
48
58
  >
49
59
  <div
50
60
  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 (props.clickable) {
79
+ (event.currentTarget as HTMLElement).click()
80
+ }
81
+ }
82
+
69
83
  function onMouseEnter() {
70
84
  hovered.value = true
71
85
  }
@@ -82,9 +96,16 @@ 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.enter.stop="onKeyDown"
104
+ @keydown.space.prevent.stop="onKeyDown"
86
105
  @mouseenter="onMouseEnter"
87
106
  @mouseleave="onMouseLeave"
107
+ @focusin="onFocus"
108
+ @focusout="onBlur"
88
109
  >
89
110
  <component
90
111
  :is="to ? NuxtLinkLocale : 'span'"