@soubiran/ui 0.3.1 → 0.5.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.
@@ -0,0 +1,36 @@
1
+ <script lang="ts">
2
+ import { tv } from 'tailwind-variants'
3
+ import { computed } from 'vue'
4
+
5
+ const container = tv({
6
+ slots: {
7
+ base: 'px-4 sm:px-6 lg:px-8',
8
+ inner: 'w-full max-w-7xl mx-auto',
9
+ },
10
+ })
11
+
12
+ export interface ContainerProps {
13
+ class?: any
14
+ ui?: Partial<typeof container.slots>
15
+ }
16
+ export interface ContainerEmits {}
17
+ export interface ContainerSlots {
18
+ default: (props: any) => any
19
+ }
20
+ </script>
21
+
22
+ <script lang="ts" setup>
23
+ const props = defineProps<ContainerProps>()
24
+ defineEmits<ContainerEmits>()
25
+ defineSlots<ContainerSlots>()
26
+
27
+ const ui = computed(() => container())
28
+ </script>
29
+
30
+ <template>
31
+ <div :class="ui.base({ class: [props.ui?.base, props.class] })">
32
+ <div :class="ui.inner({ class: props.ui?.inner })">
33
+ <slot />
34
+ </div>
35
+ </div>
36
+ </template>
@@ -0,0 +1,67 @@
1
+ <script lang="ts">
2
+ import UButton from '@nuxt/ui/components/Button.vue'
3
+ import UPopover from '@nuxt/ui/components/Popover.vue'
4
+ import { tv } from 'tailwind-variants'
5
+ import { computed, ref } from 'vue'
6
+ import thumbsUp from '~icons/ph/thumbs-up'
7
+ import useUmami from '../composables/useUmami'
8
+ import FeedbackCard from './FeedbackCard.vue'
9
+
10
+ const feedback = tv({
11
+ slots: {
12
+ base: 'px-0 py-1 text-dimmed text-sm',
13
+ },
14
+ })
15
+
16
+ export interface FeedbackProps {
17
+ id: string
18
+ class?: any
19
+ ui?: Partial<typeof feedback.slots>
20
+ }
21
+ export interface FeedbackEmits {}
22
+ export interface FeedbackSlots {}
23
+ </script>
24
+
25
+ <script lang="ts" setup>
26
+ const props = defineProps<FeedbackProps>()
27
+ defineEmits<FeedbackEmits>()
28
+ defineSlots<FeedbackSlots>()
29
+
30
+ const { track } = useUmami()
31
+ function onClick() {
32
+ track('feedback_click')
33
+ }
34
+
35
+ const content = ref('')
36
+ const rating = ref('')
37
+
38
+ function onSuccess() {
39
+ content.value = ''
40
+ rating.value = ''
41
+ }
42
+
43
+ const ui = computed(() => feedback())
44
+ </script>
45
+
46
+ <template>
47
+ <UPopover :ui="{ content: 'ring-0 data-[state=open]:animate-[scale-up_100ms_ease-out]' }">
48
+ <UButton
49
+ variant="link"
50
+ color="neutral"
51
+ label="Give feedback"
52
+ size="sm"
53
+ :icon="thumbsUp"
54
+ :class="ui.base({ class: [props.ui?.base, props.class] })"
55
+ @click="onClick"
56
+ />
57
+
58
+ <template #content>
59
+ <FeedbackCard
60
+ :id="props.id"
61
+ v-model:rating="rating"
62
+ v-model:content="content"
63
+ @success="onSuccess"
64
+ />
65
+ </template>
66
+ </UPopover>
67
+ </template>
@@ -0,0 +1,174 @@
1
+ <script lang="ts">
2
+ import UButton from '@nuxt/ui/components/Button.vue'
3
+ import UCard from '@nuxt/ui/components/Card.vue'
4
+ import UFormField from '@nuxt/ui/components/FormField.vue'
5
+ import UIcon from '@nuxt/ui/components/Icon.vue'
6
+ import UTextarea from '@nuxt/ui/components/Textarea.vue'
7
+ import UTooltip from '@nuxt/ui/components/Tooltip.vue'
8
+ import { useMutation } from '@pinia/colada'
9
+ import { motion } from 'motion-v'
10
+ import { ofetch } from 'ofetch'
11
+ import { RadioGroupIndicator, RadioGroupItem, RadioGroupRoot } from 'reka-ui'
12
+ import { tv } from 'tailwind-variants'
13
+ import { computed, ref } from 'vue'
14
+ import checkCircle from '~icons/ph/check-circle'
15
+ import useUmami from '../composables/useUmami'
16
+
17
+ const feedbackCard = tv({
18
+ slots: {
19
+ base: 'relative w-64',
20
+ successfullySentOverlay: 'absolute z-10 inset-0 bg-default',
21
+ successfullySentContent: 'absolute z-20 inset-0 flex flex-col justify-center items-center text-muted text-xs',
22
+ successfullySentIcon: 'size-5',
23
+ input: 'w-full',
24
+ radioGroup: 'flex flex-row gap-2 text-lg',
25
+ radioGroupItem: 'peer',
26
+ radioGroupLabel: 'grayscale-100 hover:grayscale-0 peer-data-[state=checked]:grayscale-0',
27
+ },
28
+ })
29
+
30
+ export interface FeedbackCardProps {
31
+ id: string
32
+ class?: any
33
+ ui?: Partial<typeof feedbackCard.slots>
34
+ }
35
+ export interface FeedbackCardEmits {
36
+ success: [void]
37
+ }
38
+ export interface FeedbackCardSlots {}
39
+ </script>
40
+
41
+ <script lang="ts" setup>
42
+ const props = defineProps<FeedbackCardProps>()
43
+ const emits = defineEmits<FeedbackCardEmits>()
44
+ defineSlots<FeedbackCardSlots>()
45
+
46
+ const content = defineModel<string>('content', { required: true })
47
+ const rating = defineModel<string>('rating', { required: true })
48
+
49
+ const ratings = [
50
+ {
51
+ label: '😭',
52
+ value: 'Hate it',
53
+ },
54
+ {
55
+ label: '🙁',
56
+ value: 'Not great',
57
+ },
58
+ {
59
+ label: '🙂',
60
+ value: 'It\'s ok',
61
+ },
62
+ {
63
+ label: '🤩',
64
+ value: 'Love it',
65
+ },
66
+ ]
67
+
68
+ const successfullySent = ref(false)
69
+
70
+ const { track } = useUmami()
71
+ const { mutate, isLoading, error } = useMutation<
72
+ void,
73
+ { rating: string, content: string },
74
+ { status: number, data: { errors?: { content?: string[], rating?: string[] } } }
75
+ >({
76
+ mutation: ({ rating, content }) => ofetch(`/api/posts/${props.id}/feedback`, {
77
+ method: 'POST',
78
+ body: { rating, content },
79
+ baseURL: import.meta.env.VITE_API_BASE_URL,
80
+ }),
81
+ onSuccess: () => {
82
+ successfullySent.value = true
83
+
84
+ track('feedback_submit')
85
+
86
+ // Wait for the animation to finish
87
+ setTimeout(emits, 200, 'success')
88
+ },
89
+ })
90
+
91
+ function sendFeedback() {
92
+ mutate({ rating: rating.value, content: content.value })
93
+ }
94
+
95
+ const formattedError = computed<string | undefined>(() => {
96
+ if (!error.value)
97
+ return undefined
98
+
99
+ if (error.value.data?.errors) {
100
+ return error.value.data.errors.content?.[0] || error.value.data.errors.rating?.[0]
101
+ }
102
+
103
+ if (error.value.status === 404)
104
+ return 'Page not found. Cannot send feedback. It\'s us, not you!'
105
+
106
+ if (error.value.status === 503)
107
+ return 'Service is currently unavailable. Please try again later.'
108
+
109
+ return 'An unexpected error occurred.'
110
+ })
111
+
112
+ const ui = computed(() => feedbackCard())
113
+ </script>
114
+
115
+ <template>
116
+ <UCard :class="ui.base({ class: [props.ui?.base, props.class] })" :ui="{ body: 'p-2 sm:p-2', footer: 'p-2 sm:p-2 flex items-center justify-between' }">
117
+ <template v-if="successfullySent">
118
+ <motion.div
119
+ :initial="{ opacity: 0 }"
120
+ :animate="{ opacity: 1, transition: { duration: 0.2 } }"
121
+ :class="ui.successfullySentOverlay({ class: props.ui?.successfullySentOverlay })"
122
+ />
123
+ <div :class="ui.successfullySentContent({ class: props.ui?.successfullySentContent })">
124
+ <motion.div
125
+ :initial="{ opacity: 0, transform: 'translateY(4px)' }"
126
+ :animate="{ opacity: 1, transform: 'translateY(0)', transition: { delay: 0.1, duration: 0.3 } }"
127
+ >
128
+ <UIcon :name="checkCircle" :class="ui.successfullySentIcon({ class: props.ui?.successfullySentIcon })" />
129
+ </motion.div>
130
+ <motion.p
131
+ :initial="{ opacity: 0, transform: 'translateY(4px)' }"
132
+ :animate="{ opacity: 1, transform: 'translateY(0)', transition: { delay: 0.2, duration: 0.3 } }"
133
+ class="mt-3"
134
+ >
135
+ Your feedback has been received.
136
+ </motion.p>
137
+ <motion.p
138
+ :initial="{ opacity: 0, transform: 'translateY(4px)' }"
139
+ :animate="{ opacity: 1, transform: 'translateY(0)', transition: { delay: 0.3, duration: 0.3 } }"
140
+ class="mt-1"
141
+ >
142
+ Thanks for your help!
143
+ </motion.p>
144
+ </div>
145
+ </template>
146
+
147
+ <UFormField :error="formattedError">
148
+ <UTextarea v-model="content" placeholder="Your feedback..." variant="soft" :class="ui.input({ class: props.ui?.input })" />
149
+ <template #error="{ error: formFieldError }">
150
+ <motion.div
151
+ v-if="formFieldError"
152
+ :initial="{ height: 0 }"
153
+ :animate="{ height: 'auto' }"
154
+ class="text-sm"
155
+ >
156
+ {{ formFieldError }}
157
+ </motion.div>
158
+ </template>
159
+ </UFormField>
160
+ <template #footer>
161
+ <RadioGroupRoot v-model="rating" :class="ui.radioGroup({ class: props.ui?.radioGroup })">
162
+ <div v-for="item in ratings" :key="item.value">
163
+ <RadioGroupItem :id="item.value" :value="item.value" :class="ui.radioGroupItem({ class: props.ui?.radioGroupItem })">
164
+ <RadioGroupIndicator />
165
+ </RadioGroupItem>
166
+ <UTooltip :text="item.value">
167
+ <label :for="item.value" :class="ui.radioGroupLabel({ class: props.ui?.radioGroupLabel })">{{ item.label }}</label>
168
+ </UTooltip>
169
+ </div>
170
+ </RadioGroupRoot>
171
+ <UButton size="sm" label="Send" :loading="isLoading" @click="sendFeedback" />
172
+ </template>
173
+ </UCard>
174
+ </template>
@@ -0,0 +1,135 @@
1
+ <script lang="ts">
2
+ import type { FunctionalComponent, SVGAttributes } from 'vue'
3
+ import UButton from '@nuxt/ui/components/Button.vue'
4
+ import USeparator from '@nuxt/ui/components/Separator.vue'
5
+ import UTooltip from '@nuxt/ui/components/Tooltip.vue'
6
+ import { tv } from 'tailwind-variants'
7
+ import { computed } from 'vue'
8
+ import discord from '~icons/simple-icons/discord'
9
+ import github from '~icons/simple-icons/github'
10
+ import linkedin from '~icons/simple-icons/linkedin'
11
+ import twitch from '~icons/simple-icons/twitch'
12
+ import x from '~icons/simple-icons/x'
13
+ import useUmami from '../composables/useUmami'
14
+ import Container from './Container.vue'
15
+
16
+ const header = tv({
17
+ slots: {
18
+ base: 'h-(--ui-header-height) flex flex-row gap-4 items-center justify-end',
19
+ link: 'p-0 text-dimmed',
20
+ },
21
+ })
22
+
23
+ export interface HeaderProps {
24
+ links: {
25
+ icon: FunctionalComponent<SVGAttributes>
26
+ label: string
27
+ to: string
28
+ }[]
29
+ class?: any
30
+ ui?: Partial<typeof header.slots>
31
+ }
32
+ export interface HeaderEmits {}
33
+ export interface HeaderSlots {}
34
+ </script>
35
+
36
+ <script lang="ts" setup>
37
+ const props = defineProps<HeaderProps>()
38
+ defineEmits<HeaderEmits>()
39
+ defineSlots<HeaderSlots>()
40
+
41
+ const { track } = useUmami()
42
+ function trackClick(label: string) {
43
+ track('header_click', { label })
44
+ }
45
+
46
+ const ui = computed(() => header())
47
+ </script>
48
+
49
+ <template>
50
+ <Container :ui="{ inner: 'max-w-5xl' }">
51
+ <header :class="ui.base({ class: [props.ui?.base, props.class] })">
52
+ <UTooltip
53
+ v-for="link of props.links"
54
+ :key="link.label"
55
+ :text="link.label"
56
+ >
57
+ <UButton
58
+ variant="link"
59
+ color="neutral"
60
+ :to="link.to"
61
+ :aria-label="link.label"
62
+ :icon="link.icon"
63
+ :class="ui.link({ class: props.ui?.link })"
64
+ @click="trackClick(link.label)"
65
+ />
66
+ </UTooltip>
67
+ <USeparator orientation="vertical" class="h-5" />
68
+ <UTooltip text="LinkedIn">
69
+ <UButton
70
+ href="https://www.linkedin.com/in/esteban25/"
71
+ target="_blank"
72
+ variant="link"
73
+ color="neutral"
74
+ aria-label="LinkedIn"
75
+ :icon="linkedin"
76
+ :class="ui.link({ class: props.ui?.link })"
77
+ :ui="{ leadingIcon: 'size-4' }"
78
+ @click="trackClick('LinkedIn')"
79
+ />
80
+ </UTooltip>
81
+ <UTooltip text="X">
82
+ <UButton
83
+ href="https://x.com/soubiran_"
84
+ target="_blank"
85
+ variant="link"
86
+ color="neutral"
87
+ aria-label="X"
88
+ :icon="x"
89
+ :class="ui.link({ class: props.ui?.link })"
90
+ :ui="{ leadingIcon: 'size-4' }"
91
+ @click="trackClick('X')"
92
+ />
93
+ </UTooltip>
94
+ <UTooltip text="Twitch">
95
+ <UButton
96
+ href="https://www.twitch.tv/barbapapazes"
97
+ target="_blank"
98
+ variant="link"
99
+ color="neutral"
100
+ aria-label="Twitch"
101
+ :icon="twitch"
102
+ :class="ui.link({ class: props.ui?.link })"
103
+ :ui="{ leadingIcon: 'size-4' }"
104
+ @click="trackClick('Twitch')"
105
+ />
106
+ </UTooltip>
107
+ <UTooltip text="GitHub">
108
+ <UButton
109
+ href="https://github.com/barbapapazes"
110
+ target="_blank"
111
+ variant="link"
112
+ color="neutral"
113
+ aria-label="GitHub"
114
+ :icon="github"
115
+ :class="ui.link({ class: props.ui?.link })"
116
+ :ui="{ leadingIcon: 'size-4' }"
117
+ @click="trackClick('GitHub')"
118
+ />
119
+ </UTooltip>
120
+ <UTooltip text="Discord">
121
+ <UButton
122
+ href="https://discord.gg/q2ghCGUuFR"
123
+ target="_blank"
124
+ variant="link"
125
+ color="neutral"
126
+ aria-label="Discord"
127
+ :icon="discord"
128
+ :class="ui.link({ class: props.ui?.link })"
129
+ :ui="{ leadingIcon: 'size-4' }"
130
+ @click="trackClick('Discord')"
131
+ />
132
+ </UTooltip>
133
+ </header>
134
+ </Container>
135
+ </template>
@@ -0,0 +1,63 @@
1
+ <script lang="ts">
2
+ import { tv } from 'tailwind-variants'
3
+ import { computed } from 'vue'
4
+ import Container from './Container.vue'
5
+
6
+ const page = tv({
7
+ slots: {
8
+ base: 'py-12',
9
+ header: 'max-w-3xl mx-auto xl:max-w-none xl:grid xl:grid-cols-[256px_768px_256px]',
10
+ headerInner: 'xl:col-start-2',
11
+ content: 'mt-6 max-w-3xl mx-auto xl:max-w-none xl:grid xl:grid-cols-[256px_768px_256px] xl:mx-auto',
12
+ contentInner: 'xl:col-start-2',
13
+ right: 'hidden xl:block xl:col-start-3 xl:pl-8 xl:h-full',
14
+ rightInner: 'sticky top-4',
15
+ },
16
+ })
17
+
18
+ export interface PageProps {
19
+ class?: any
20
+ ui?: Partial<typeof page.slots>
21
+ }
22
+ export interface PageEmits {}
23
+ export interface PageSlots {
24
+ default: (props: any) => any
25
+ header: (props: any) => any
26
+ right: (props: any) => any
27
+ bottom: (props: any) => any
28
+ }
29
+ </script>
30
+
31
+ <script lang="ts" setup>
32
+ const props = defineProps<PageProps>()
33
+ defineEmits<PageEmits>()
34
+ defineSlots<PageSlots>()
35
+
36
+ const ui = computed(() => page())
37
+ </script>
38
+
39
+ <template>
40
+ <div :class="ui.base({ class: [props.ui?.base, props.class] })">
41
+ <Container>
42
+ <div :class="ui.header({ class: props.ui?.header })">
43
+ <div :class="ui.headerInner({ class: props.ui?.headerInner })">
44
+ <slot name="header" />
45
+ </div>
46
+ </div>
47
+
48
+ <div :class="ui.content({ class: props.ui?.content })">
49
+ <div :class="ui.contentInner({ class: props.ui?.contentInner })">
50
+ <slot />
51
+ </div>
52
+
53
+ <div :class="ui.right({ class: props.ui?.right })">
54
+ <div :class="ui.rightInner({ class: props.ui?.rightInner })">
55
+ <slot name="right" />
56
+ </div>
57
+ </div>
58
+ </div>
59
+ </Container>
60
+
61
+ <slot name="bottom" />
62
+ </div>
63
+ </template>
@@ -0,0 +1,36 @@
1
+ <script lang="ts">
2
+ import { tv } from 'tailwind-variants'
3
+ import { computed } from 'vue'
4
+ import PageTitle from './PageTitle.vue'
5
+
6
+ const pageHeader = tv({
7
+ slots: {
8
+ base: '',
9
+ },
10
+ })
11
+
12
+ export interface PageHeaderProps {
13
+ title: string
14
+ class?: any
15
+ ui?: Partial<typeof pageHeader.slots>
16
+ }
17
+ export interface PageHeaderEmits {}
18
+ export interface PageHeaderSlots {
19
+ after: (props: any) => any
20
+ }
21
+ </script>
22
+
23
+ <script lang="ts" setup>
24
+ const props = defineProps<PageHeaderProps>()
25
+ defineEmits<PageHeaderEmits>()
26
+ defineSlots<PageHeaderSlots>()
27
+
28
+ const ui = computed(() => pageHeader())
29
+ </script>
30
+
31
+ <template>
32
+ <div :class="ui.base({ class: [props.ui?.base, props.class] })">
33
+ <PageTitle :title="props.title" />
34
+ <slot name="after" />
35
+ </div>
36
+ </template>
@@ -0,0 +1,32 @@
1
+ <script lang="ts">
2
+ import { tv } from 'tailwind-variants'
3
+ import { computed } from 'vue'
4
+
5
+ const pageTitle = tv({
6
+ slots: {
7
+ base: 'text-xl font-bold text-highlighted',
8
+ },
9
+ })
10
+
11
+ export interface PageTitleProps {
12
+ title: string
13
+ class?: any
14
+ ui?: Partial<typeof pageTitle.slots>
15
+ }
16
+ export interface PageTitleEmits {}
17
+ export interface PageTitleSlots {}
18
+ </script>
19
+
20
+ <script lang="ts" setup>
21
+ const props = defineProps<PageTitleProps>()
22
+ defineEmits<PageTitleEmits>()
23
+ defineSlots<PageTitleSlots>()
24
+
25
+ const ui = computed(() => pageTitle())
26
+ </script>
27
+
28
+ <template>
29
+ <h1 :class="ui.base({ class: [props.ui?.base, props.class] })">
30
+ {{ title }}
31
+ </h1>
32
+ </template>
@@ -0,0 +1,83 @@
1
+ <script lang="ts">
2
+ import UIcon from '@nuxt/ui/components/Icon.vue'
3
+ import { tv } from 'tailwind-variants'
4
+ import { computed } from 'vue'
5
+ import github from '~icons/simple-icons/github'
6
+ import linkedin from '~icons/simple-icons/linkedin'
7
+ import twitch from '~icons/simple-icons/twitch'
8
+ import x from '~icons/simple-icons/x'
9
+ import youtube from '~icons/simple-icons/youtube'
10
+
11
+ const socials = tv({
12
+ slots: {
13
+ base: 'flex flex-wrap gap-2',
14
+ link: 'flex items-center gap-1',
15
+ icon: 'size-4',
16
+ },
17
+ })
18
+
19
+ export interface SocialsProps {
20
+ class?: any
21
+ ui?: Partial<typeof socials.slots>
22
+ }
23
+ export interface SocialsEmits {}
24
+ export interface SocialsSlots {}
25
+ </script>
26
+
27
+ <script lang="ts" setup>
28
+ const props = defineProps<SocialsProps>()
29
+ defineEmits<SocialsEmits>()
30
+ defineSlots<SocialsSlots>()
31
+
32
+ const ui = computed(() => socials())
33
+ </script>
34
+
35
+ <template>
36
+ <p :class="ui.base({ class: [props.ui?.base, props.class] })">
37
+ <a
38
+ href="https://www.linkedin.com/in/esteban25/"
39
+ rel="noopener noreferrer"
40
+ target="_blank"
41
+ :class="ui.link({ class: props.ui?.link })"
42
+ >
43
+ <UIcon :name="linkedin" :class="ui.icon({ class: props.ui?.icon })" />
44
+ <span>LinkedIn</span>
45
+ </a>
46
+ <a
47
+ href="https://x.com/soubiran_"
48
+ rel="noopener noreferrer"
49
+ target="_blank"
50
+ :class="ui.link({ class: props.ui?.link })"
51
+ >
52
+ <UIcon :name="x" :class="ui.icon({ class: props.ui?.icon })" />
53
+ <span>X (Twitter)</span>
54
+ </a>
55
+ <a
56
+ href="https://github.com/barbapapazes"
57
+ rel="noopener noreferrer"
58
+ target="_blank"
59
+ :class="ui.link({ class: props.ui?.link })"
60
+ >
61
+ <UIcon :name="github" :class="ui.icon({ class: props.ui?.icon })" />
62
+ <span>GitHub</span>
63
+ </a>
64
+ <a
65
+ href="https://www.twitch.tv/barbapapazes"
66
+ rel="noopener noreferrer"
67
+ target="_blank"
68
+ :class="ui.link({ class: props.ui?.link })"
69
+ >
70
+ <UIcon :name="twitch" :class="ui.icon({ class: props.ui?.icon })" />
71
+ <span>Twitch</span>
72
+ </a>
73
+ <a
74
+ href="https://www.youtube.com/@barbapapazes"
75
+ target="_blank"
76
+ rel="noopener noreferrer"
77
+ :class="ui.link({ class: props.ui?.link })"
78
+ >
79
+ <UIcon :name="youtube" :class="ui.icon({ class: props.ui?.icon })" />
80
+ <span>YouTube</span>
81
+ </a>
82
+ </p>
83
+ </template>
@@ -0,0 +1,34 @@
1
+ <script lang="ts">
2
+ import { tv } from 'tailwind-variants'
3
+ import { computed } from 'vue'
4
+
5
+ const sponsors = tv({
6
+ slots: {
7
+ base: 'not-prose',
8
+ img: 'border-0',
9
+ },
10
+ })
11
+
12
+ export interface SponsorsProps {
13
+ class?: any
14
+ ui?: Partial<typeof sponsors.slots>
15
+ }
16
+ export interface SponsorsEmits {}
17
+ export interface SponsorsSlots {}
18
+ </script>
19
+
20
+ <script lang="ts" setup>
21
+ const props = defineProps<SponsorsProps>()
22
+ defineEmits<SponsorsEmits>()
23
+ defineSlots<SponsorsSlots>()
24
+
25
+ const ui = computed(() => sponsors())
26
+ </script>
27
+
28
+ <template>
29
+ <p :class="ui.base({ class: [props.ui?.base, props.class] })">
30
+ <a href="https://soubiran.dev/sponsorship?utm_source=infra.soubiran.dev&utm_medium=link" target="_blank">
31
+ <img src="https://raw.githubusercontent.com/Barbapapazes/static/refs/heads/main/sponsors.svg" alt="Sponsors" :class="ui.img({ class: props.ui?.img })">
32
+ </a>
33
+ </p>
34
+ </template>