Float transparent PNG over text - javascript

I am looking to add semi-transparent stamp type image over a particular part of the text.
We are using vue to build our new website.
I've not really found anything about how to do this in vue.
I'm looking to place the image over the 4th line of the code shown below. Basically what it does is display the 'Report accepted by:' text along with the person who accepted the report taken from the database. I'd like to display that with an overlapping image that resembles a stamp of approval.
<template>
<div class="clearfix">
<span>Report accepted by:</span>
<span v-if="report_info.accepted && report_info.accepted_by !== null">{{ memberById(report_info.accepted_by).callsign }}</span>
<button
v-if="isAdmin"
class="float-right"
v-on:click="acceptRejectReport"
>{{ acceptButtonText }}</button>
</div>
</template>
<script>
import { mapState, mapGetters } from "vuex"
export default {
name: "ReportApprovalComp",
mounted () {
this.checkAdmin();
},
data () {
return {
isAdmin: false
}
},
computed: {
acceptButtonText() {
if(this.report_info.accepted){
return "Revoke report acceptance";
} else {
return "Approve report";
}
},
...mapState("missionStore", {
report_info: state => state.report,
}),
...mapGetters("missionStore", [
"memberById"
])
},
methods: {
checkAdmin: async function () {
this.isAdmin = await this.$auth.isAdmin(this.$options.name);
},
acceptRejectReport: async function () {
this.$store.dispatch('missionStore/acceptRejectReport',
{
caller: this.$options.name,
member_id: await this.$auth.getUserId(),
});
}
}
}
</script>
<style scoped>
</style>

already has the logic ... you just need to actually provide your img element as the last child inside that span. Your span will need to have css position:relative and your img needs css position:absolute;top:0;right:0; ... you might need display:inline-block on your span
<span v-if="report_info.accepted && report_info.accepted_by !== null"
style="position:relative;display:inline-block;">
{{ memberById(report_info.accepted_by).callsign }}
<img style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
width:100%;height:auto;" src="web path to img file"/>
</span>

Related

Why does boostrap vue toaster dissapears immediately?

I am using Laravel + Vue.js to create a SPA. In the SPA, I am creating a form where user can write markdown content in it and click button to submit it. I want to show an error toaster at the bottom right corner of the screen if the user didn't input any content when they clicked the send button.
I am using boostrap vue toast to implement the error toaster.
However, when I clicked the send button, the error toaster will just blink for 1 second and dissapear immediately. Also, the error toaster blinks at top left corner which is different from what I wrote in the code below.
The mixin that contains the method to invoke the toast:
ErrorMessage.vue
# ErrorMessage.vue
<script>
export default {
methods: {
showErrorMessage (msg) {
this.$bvToast.toast(msg, {
title: ["Error!"],
variant: "danger",
toaster: "b-toaster-bottom-right"
});
}
}
};
</script>
I imported the above mixin in this vue component ArticleForm.vue.
ArticleForm.vue
<template>
<form #submit.prevent="passArticleData">
<div id="editor-markdown-editor">
<div id="editor-markdown">
<div
id="editor-markdown-tag-input"
#click="toggleTagModal"
>
<ul v-if="insertedTags.length">
<li v-for="tag in insertedTags"
:key="tag.id"
class="tag"
>
{{ tag.name }}
</li>
</ul>
<span v-else>タグを入力してください</span>
</div>
<div id="editor-markdown-textarea">
<textarea :value="input" #input="update" ref="textarea"></textarea>
<div id="drop-here"></div>
</div>
</div>
<div id="editor-preview">
<article v-html="compiledMarkdown(input)" class="markdown-render" ref="articleHtml"></article>
</div>
</div>
</form>
</template>
<script>
import _ from "lodash";
import ErrorMessage from "./mixins/ErrorMessage";
import { markdownTable } from "markdown-table";
export default {
props: {
article: Object,
tags: Array
},
mixins: [ErrorMessage],
data () {
return {
articleToPass: {
title: "",
body: "",
isDraft: true
},
input: "",
tagsToPass: [],
insertedTags: [],
tagModalOpen: false
};
},
methods: {
update: _.debounce(function (e) {
this.input = e.target.value;
}, 300),
passArticleData (e) {
// Get title from input
try {
this.articleToPass.title = this.$refs.articleHtml.getElementsByTagName("h1").item(0).innerHTML;
} catch (e) {
this.showErrorMessage(["Body md can't be blank"]);
}
// Remove first line(title) from users' input
this.articleToPass.body = this.input.substring(this.input.indexOf("\n") + 1);
// tag id of written article
const tagIds = this.insertedTags.map(obj => obj.id);
this.tagsToPass = tagIds;
this.$emit("handle-new-data", this.articleToPass, this.tagsToPass);
}
}
Parent component of the above vue component:
ArticleCreate.vue
<template>
<div id="content-area">
<header-component></header-component>
<main id="editor-area">
<article-form :article="article" :tags="tags" #handle-new-data="postArticle"></article-form>
</main>
</div>
</template>
<script>
import HeaderComponent from "./Header.vue";
import ArticleForm from "./ArticleForm.vue";
export default {
data () {
return {
article: {
title: "",
body: "",
is_draft: true
},
tags: []
};
},
components: {
HeaderComponent,
ArticleForm
},
methods: {
postArticle (articleObj, tagsData) {
const data = { article: articleObj, tags: tagsData };
axios.post("/api/articles", data)
.then((res) => {
this.$router.push({ name: "article.show", params: { article: res.data } });
});
}
}
};
</script>
I tried:
changed this.$bvToast to this.$root.$bvToast (based on this issue)
downgrade my bootstrap version from 4.6.0 to 4.5.3 (based on this question)
I have spent trying to solve this but failed. Does anyone know why is this happening and how can I make it work? Please let me know if you need any extra information. Thank you in advanced.
As stated in this comment on stackoverflow that is usually a sign of a bootstrap version mismatch.
I was actually able to reproduce that issue and also fix it with rolling back to bootstrap v4
Broken with bootstrap 5
https://codesandbox.io/s/bootstrap-vue-toasting-broken-with-bootstrap-5-bqe2c
Working with bootstrap 4
https://codesandbox.io/s/bootstrap-vue-toasting-working-with-bootstrap-4-jk2jl
I have figured out the problem.
The problem is that bootstrap-vue css is not loaded properly in my project.
Just add
import "bootstrap-vue/dist/bootstrap-vue.css";
to app.js and it works perfectly fine now.
Thank you

Automatic resizing of textarea after loading data in Vue

I have a Vue page which loads an json array from an api and displays the content in a list of multiple s by using v-for.
If you focus on one of the textarea's or change the text a function automatically resize's the textarea to fit the content.
<div v-for="post in posts">
<textarea v-model="post.body" rows="1" #focus="resizeTextarea" #keyup="resizeTextarea"></textarea>
</div>
resizeTextarea(e) {
let area = e.target;
area.style.overflow = 'hidden';
area.style.height = area.scrollHeight + 'px';
}
With my limited Vue knowledge, I can't find a solution to automatically resize all textarea's after loading the data from the API. There is no #load on a textarea.
I was trying to reach the same goal with using a watcher on the data but it feels like a long workaround.
Anyone a descent solution? Thank you!
https://jsfiddle.net/oehoe83/c1b8frup/19/
One solution would be to create a component for your textarea element and then resize it in the mounted() hook. Here's an example using single-file components:
// CustomTextarea.vue
<template>
<textarea
v-model="value"
ref="textarea"
rows="1"
#focus="resize"
#keyup="resize"
>
</textarea>
</template>
<script>
export default {
props: {
value: {
type: String,
required: true,
}
},
mounted() {
this.resize();
},
methods: {
resize() {
const { textarea } = this.$refs;
textarea.style.height = textarea.scrollHeight - 4 + 'px';
}
}
}
</script>
Then in your parent:
<template>
<div v-for="post in posts">
<CustomTextarea v-model="post.body" />
</div>
</template>
<script>
import CustomTextarea from './CustomTextarea.vue';
export default {
components: {
CustomTextarea,
}
// etc.
}
</script>
Note: if you're using Vue 3, replace value with modelValue in the child component.
Alternatively you could use a watch like you suggested, there's nothing wrong with that. Something like this:
watch: {
posts() {
// Wait until the template has updated
this.$nextTick(() => {
[...document.querySelectorAll('textarea')].forEach(textarea => {
this.resizeTextarea({ target: textarea });
});
});
}
}
you can add the ref attribute :
<div id="app">
<div v-for="post in posts" ref="container">
<textarea v-model="post.body" rows="1"#focus="resizeTextarea" #keyup="resizeTextarea" ></textarea>
</div>
</div>
and add the following code at the end of mounted() :
this.$nextTick(()=>{
this.$refs.container.forEach( ta => {
ta.firstChild.dispatchEvent(new Event("keyup"));
});
});

In Vue2 tag input, trying to display tag name from tag object in autocomplete

I have a Vue component with a tag input where I make an ajax call to the db to retrieve suggestions as the user is typing. I am using #johmun/vue-tags-input for this. Everything works fine except that instead of the autocomplete listing options including only the tag attribute of the Tag model, it includes the entire object.
I want to list only the tag attribute in the view, but I want to reference the array of entire Tag objects when it comes time to create the association with the user.
This what the current dropdown look like in the browser:
Here is my input component removing the irrelevant parts, so it meets SO's size constraints:
<template>
<div >
<b-container class="mt-8 pb-5">
<b-row class="justify-content-center">
<b-col lg="5" md="7">
<form>
...
<div v-if="step === 3">
<h2><strong>What topics are you interested in?</strong> (e.g tag1, tag2, etc...)</h2>
<h2>A few popular ones:
<button #click.prevent="addToTags(item)" class="btn btn-sm btn-success" v-for="item in existingTags.slice(0, 3)" :key="item.id">
{{ item.tag }}
</button>
</h2>
<vue-tags-input
v-model="tag"
v-on:keyup.native="getTags"
:tags="tags"
:autocomplete-items="filteredItems"
:autocomplete-min-length=3
#tags-changed="confirmedTags"
/>
</div>
...
</form>
</b-col>
</b-row>
</b-container>
</div>
</template>
<script>
import VueTagsInput from '#johmun/vue-tags-input';
import UsersService from '#/services/UsersService'
import TagsService from '#/services/TagsService'
import TagRelationsService from '#/services/TagRelationsService'
export default {
name: 'UserOnboard',
data() {
return {
tag: '',
tags: [],
...
}
};
},
components: {
VueTagsInput
},
computed: {
filteredItems() {
return this.existingTags.filter(i => {
return i.tag.toLowerCase().indexOf(this.tag.toLowerCase()) !== -1;
});
},
...
user() {
return this.$store.state.auth.user
},
existingTags() {
return this.$store.state.tags.existingTags
}
},
...
methods:{
...
},
addToTags(newTag) {
if (!this.tags.includes(newTag)) {
this.tags.push(newTag)
}
// on button click add appropriate tag to tags array
// console.log('tag array is: ',tags)
},
confirmedTags(event) {
this.tags=event
console.log(event)
},
...
getTags() { //debounce need to be inside conditional
console.log('gettin tags')
// if (this.tag.length >2) {
this.$store.dispatch('debounceTags', this.tag)
// }
}
}
}
</script>
Also, here is the debounceTags method which runs via vuex:
import TagsService from '#/services/TagsService'
import { debounce } from "lodash";
export const state = {
existingTags: []
}
export const mutations = {
setTags (state, tags) {
state.existingTags = tags
}
}
export const actions = {
debounceTags: debounce(({ dispatch }, data) => {
console.log("Inside debounced function.");
dispatch("getTags" ,data);
}, 300),
async getTags ({ commit }, data) {
await TagsService.getTags(data)
.then(function (response) {
console.log('before setting tags this is resp.data: ', response)
commit('setTags', response);
});
}
}
export const getters = {
}

Vue.js: conflict v-html and a function

Here is the template of the component for a notification:
<template>
<div>
<li class="g-line-height-1_2">
<router-link :to="linkFromNotification(item)"
#click.native="readNotification(item)"
v-html="item.message"
:class="activeNotification">
<br>
<span class="g-font-size-12 g-color-gray-dark-v5">
{{ getTime }}
</span>
</router-link>
</li>
<li>
<hr class="g-my-0">
</li>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import moment from 'moment';
export default {
props: ['item'],
computed: {
...mapGetters([
'getLastNotifications',
'getNotifications',
]),
activeNotification() {
if (this.item.viewed === true) {
return 'nav-link g-bg-gray-light-v5--hover g-px-20 g-py-10 u-link-v5'
} else {
return 'nav-link g-bg-primary-opacity-x--hover g-bg-primary-opacity-x2 g-px-20 g-py-10 u-link-v5'
}
},
getTime() {
moment.locale('ru');
return moment(this.item.created_at, 'YYYY-MM-DD HH:mm:ss Z').fromNow();
},
},
methods: {
...mapActions([
'notifications',
'readNotification'
]),
linkFromNotification(item) {
if (item.notification_type === 'user_subscribed') {
return {name: 'person', params: {id: item.object_id}}
} else if (['comment_created', 'answer_selected', 'answer_created'].includes(item.notification_type)) {
// TODO: link must be constructed with hash
return `/posts/${item.object_id}#${item.anchor}`;
} else if (item.notification_type === 'user_coauthored') {
return {name: 'show_post', params: {id: item.object_id}}
}
}
}
}
</script>
Every notification gets a text from a server using v-html. Now I can see the text of the notification but I cannot see the time of it. It seems like my function getTime is overlapped by something in the router-link. When I try to put <span> with the function getTime under the router-link, the notification time is displayed but my page layout is getting collapsed. Please help!
v-html sets the content of the element it is attached to. Any content inside the containing tags will be overwritten. To add HTML to existing content, make a <span> where you want the HTML inserted and put your v-html on that span.

Mouseover or hover vue.js

I would like to show a div when hovering over an element in vue.js. But I can't seem to get it working.
It looks like there is no event for hover or mouseover in vue.js. Is this really true?
Would it be possible to combine jquery hover and vue methods?
i feel above logics for hover is incorrect. it just inverse when mouse hovers. i have used below code. it seems to work perfectly alright.
<div #mouseover="upHere = true" #mouseleave="upHere = false" >
<h2> Something Something </h2>
<some-component v-show="upHere"></some-component>
</div>
on vue instance
data: {
upHere: false
}
Here is a working example of what I think you are asking for.
http://jsfiddle.net/1cekfnqw/3017/
<div id="demo">
<div v-show="active">Show</div>
<div #mouseover="mouseOver">Hover over me!</div>
</div>
var demo = new Vue({
el: '#demo',
data: {
active: false
},
methods: {
mouseOver: function(){
this.active = !this.active;
}
}
});
There's no need for a method here.
HTML
<div v-if="active">
<h2>Hello World!</h2>
</div>
<div v-on:mouseover="active = !active">
<h1>Hover me!</h1>
</div>
JS
new Vue({
el: 'body',
data: {
active: false
}
})
To show child or sibling elements it's possible with CSS only. If you use :hover before combinators (+, ~, >, space). Then the style applies not to hovered element.
HTML
<body>
<div class="trigger">
Hover here.
</div>
<div class="hidden">
This message shows up.
</div>
</body>
CSS
.hidden { display: none; }
.trigger:hover + .hidden { display: inline; }
With mouseover and mouseleave events you can define a toggle function that implements this logic and react on the value in the rendering.
Check this example:
var vm = new Vue({
el: '#app',
data: {btn: 'primary'}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<div id='app'>
<button
#mouseover="btn='warning'"
#mouseleave="btn='primary'"
:class='"btn btn-block btn-"+btn'>
{{ btn }}
</button>
</div>
I think what you want to achieve is with the combination of
#mouseover, #mouseout, #mouseenter and #mouseleave
So the two best combinations are
"#mouseover and #mouseout"
or
"#mouseenter and #mouseleave"
And I think, It's better to use the 2nd pair so that you can achieve the hover effect and call functionalities on that.
<div #mouseenter="activeHover = true" #mouseleave="activeHover = false" >
<p v-if="activeHover"> This will be showed on hover </p>
<p v-if ="!activeHover"> This will be showed in simple cases </p>
</div>
on vue instance
data : {
activeHover : false
}
Note: 1st pair will effect/travel on the child elements as well but 2nd pair will only effect where you want to use it not the child elements. Else you will experience some glitch/fluctuation by using 1st pair. So, better to use 2nd pair to avoid any fluctuations.
I hope, it will help others as well :)
Though I would give an update using the new composition api.
Component
<template>
<div #mouseenter="hovering = true" #mouseleave="hovering = false">
{{ hovering }}
</div>
</template>
<script>
import { ref } from '#vue/composition-api'
export default {
setup() {
const hovering = ref(false)
return { hovering }
}
})
</script>
Reusable Composition Function
Creating a useHover function will allow you to reuse in any components.
export function useHover(target: Ref<HTMLElement | null>) {
const hovering = ref(false)
const enterHandler = () => (hovering.value = true)
const leaveHandler = () => (hovering.value = false)
onMounted(() => {
if (!target.value) return
target.value.addEventListener('mouseenter', enterHandler)
target.value.addEventListener('mouseleave', leaveHandler)
})
onUnmounted(() => {
if (!target.value) return
target.value.removeEventListener('mouseenter', enterHandler)
target.value.removeEventListener('mouseleave', leaveHandler)
})
return hovering
}
Here's a quick example calling the function inside a Vue component.
<template>
<div ref="hoverRef">
{{ hovering }}
</div>
</template>
<script lang="ts">
import { ref } from '#vue/composition-api'
import { useHover } from './useHover'
export default {
setup() {
const hoverRef = ref(null)
const hovering = useHover(hoverRef)
return { hovering, hoverRef }
}
})
</script>
You can also use a library such as #vuehooks/core which comes with many useful functions including useHover.
Reference: Vuejs composition API
It's possible to toggle a class on hover strictly within a component's template, however, it's not a practical solution for obvious reasons. For prototyping on the other hand, I find it useful to not have to define data properties or event handlers within the script.
Here's an example of how you can experiment with icon colors using Vuetify.
new Vue({
el: '#app'
})
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/vuetify/dist/vuetify.min.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify/dist/vuetify.js"></script>
<div id="app">
<v-app>
<v-toolbar color="black" dark>
<v-toolbar-items>
<v-btn icon>
<v-icon #mouseenter="e => e.target.classList.toggle('pink--text')" #mouseleave="e => e.target.classList.toggle('pink--text')">delete</v-icon>
</v-btn>
<v-btn icon>
<v-icon #mouseenter="e => e.target.classList.toggle('blue--text')" #mouseleave="e => e.target.classList.toggle('blue--text')">launch</v-icon>
</v-btn>
<v-btn icon>
<v-icon #mouseenter="e => e.target.classList.toggle('green--text')" #mouseleave="e => e.target.classList.toggle('green--text')">check</v-icon>
</v-btn>
</v-toolbar-items>
</v-toolbar>
</v-app>
</div>
With mouseover only the element stays visible when mouse leaves the hovered element, so I added this:
#mouseover="active = !active" #mouseout="active = !active"
<script>
export default {
data(){
return {
active: false
}
}
</script>
I came up with the same problem, and I work it out !
<img :src='book.images.small' v-on:mouseenter="hoverImg">
There is a correct working JSFiddle: http://jsfiddle.net/1cekfnqw/176/
<p v-on:mouseover="mouseOver" v-bind:class="{on: active, 'off': !active}">Hover over me!</p>
Please take a look at the vue-mouseover package if you are not satisfied with how does this code look:
<div
#mouseover="isMouseover = true"
#mouseleave="isMouseover = false"
/>
vue-mouseover provides a v-mouseover directive that automaticaly updates the specified data context property when the cursor enters or leaves an HTML element the directive is attached to.
By default in the next example isMouseover property will be true when the cursor is over an HTML element and false otherwise:
<div v-mouseover="isMouseover" />
Also by default isMouseover will be initially assigned when v-mouseover is attached to the div element, so it will not remain unassigned before the first mouseenter/mouseleave event.
You can specify custom values via v-mouseover-value directive:
<div
v-mouseover="isMouseover"
v-mouseover-value="customMouseenterValue"/>
or
<div
v-mouseover="isMouseover"
v-mouseover-value="{
mouseenter: customMouseenterValue,
mouseleave: customMouseleaveValue
}"
/>
Custom default values can be passed to the package via options object during setup.
Here is a very simple example for MouseOver and MouseOut:
<div id="app">
<div :style = "styleobj" #mouseover = "changebgcolor" #mouseout = "originalcolor">
</div>
</div>
new Vue({
el:"#app",
data:{
styleobj : {
width:"100px",
height:"100px",
backgroundColor:"red"
}
},
methods:{
changebgcolor : function() {
this.styleobj.backgroundColor = "green";
},
originalcolor : function() {
this.styleobj.backgroundColor = "red";
}
}
});
This worked for me for nuxt
<template>
<span
v-if="item"
class="primary-navigation-list-dropdown"
#mouseover="isTouchscreenDevice ? null : openDropdownMenu()"
#mouseleave="isTouchscreenDevice ? null : closeDropdownMenu()"
>
<nuxt-link
to="#"
#click.prevent.native="openDropdownMenu"
v-click-outside="closeDropdownMenu"
:title="item.title"
:class="[
item.cssClasses,
{ show: isDropdownMenuVisible }
]"
:id="`navbarDropdownMenuLink-${item.id}`"
:aria-expanded="[isDropdownMenuVisible ? true : false]"
class="
primary-navigation-list-dropdown__toggle
nav-link
dropdown-toggle"
aria-current="page"
role="button"
data-toggle="dropdown"
>
{{ item.label }}
</nuxt-link>
<ul
:class="{ show: isDropdownMenuVisible }"
:aria-labelledby="`navbarDropdownMenuLink-${item.id}`"
class="
primary-navigation-list-dropdown__menu
dropdown-menu-list
dropdown-menu"
>
<li
v-for="item in item.children" :key="item.id"
class="dropdown-menu-list__item"
>
<NavLink
:attributes="item"
class="dropdown-menu-list__link dropdown-item"
/>
</li>
</ul>
</span>
</template>
<script>
import NavLink from '#/components/Navigation/NavLink';
export default {
name: "DropdownMenu",
props: {
item: {
type: Object,
required: true,
},
},
data() {
return {
isDropdownMenuVisible: false,
isTouchscreenDevice: false
};
},
mounted() {
this.detectTouchscreenDevice();
},
methods: {
openDropdownMenu() {
if (this.isTouchscreenDevice) {
this.isDropdownMenuVisible = !this.isDropdownMenuVisible;
} else {
this.isDropdownMenuVisible = true;
}
},
closeDropdownMenu() {
if (this.isTouchscreenDevice) {
this.isDropdownMenuVisible = false;
} else {
this.isDropdownMenuVisible = false;
}
},
detectTouchscreenDevice() {
if (window.PointerEvent && ('maxTouchPoints' in navigator)) {
if (navigator.maxTouchPoints > 0) {
this.isTouchscreenDevice = true;
}
} else {
if (window.matchMedia && window.matchMedia("(any-pointer:coarse)").matches) {
this.isTouchscreenDevice = true;
} else if (window.TouchEvent || ('ontouchstart' in window)) {
this.isTouchscreenDevice = true;
}
}
return this.isTouchscreenDevice;
}
},
components: {
NavLink
}
};
</script>
<style scoped lang="scss">
.primary-navigation-list-dropdown {
&__toggle {
color: $white;
&:hover {
color: $blue;
}
}
&__menu {
margin-top: 0;
}
&__dropdown {
}
}
.dropdown-menu-list {
&__item {
}
&__link {
&.active,
&.nuxt-link-exact-active {
border-bottom: 1px solid $blue;
}
}
}
</style>
You can also use VueUse composables.
This is for mouse hover
<script setup>
import { useElementHover } from '#vueuse/core'
const myHoverableElement = ref()
const isHovered = useElementHover(myHoverableElement)
</script>
<template>
<button ref="myHoverableElement">
{{ isHovered }}
</button>
</template>
This one for mouse hover
import { useMouse } from '#vueuse/core'
const { x, y, sourceType } = useMouse()
or even in a specific element.
TLDR: quite a few handy composables for your Vue2/Vue3 apps!

Categories

Resources