mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-14 12:14:41 +01:00
feat: support presets (#14)
This commit is contained in:
@@ -2,6 +2,7 @@ export default {
|
|||||||
entries: [
|
entries: [
|
||||||
'./src/index',
|
'./src/index',
|
||||||
{ input: './src/components/', outDir: 'dist/components', ext: 'js' },
|
{ input: './src/components/', outDir: 'dist/components', ext: 'js' },
|
||||||
|
{ input: './src/presets/', outDir: 'dist/presets', ext: 'js' },
|
||||||
{ input: './src/utils/', outDir: 'dist/utils', ext: 'js' }
|
{ input: './src/utils/', outDir: 'dist/utils', ext: 'js' }
|
||||||
],
|
],
|
||||||
declaration: true,
|
declaration: true,
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
import { UseDark } from '@vueuse/components'
|
import { UseDark } from '@vueuse/components'
|
||||||
|
|
||||||
const sections = [
|
const sections = [
|
||||||
{ label: 'Getting Started', links: [{ label: 'Installation', to: '/' }, { label: 'Examples', to: '/examples' }, { label: 'Migration', to: '/migration' }, { label: 'Dark mode', to: '/dark' }] },
|
{ label: 'Getting Started', links: [{ label: 'Usage', to: '/' }, { label: 'Examples', to: '/examples' }, { label: 'Migration', to: '/migration' }, { label: 'Dark mode', to: '/dark' }] },
|
||||||
{ label: 'Elements', links: [{ label: 'Avatar', to: '/components/Avatar' }, { label: 'AvatarGroup', to: '/components/AvatarGroup' }, { label: 'Badge', to: '/components/Badge' }, { label: 'Button', to: '/components/Button' }, { label: 'Dropdown', to: '/components/Dropdown' }, { label: 'Icon', to: '/components/Icon' }, { label: 'Link', to: '/components/Link' }, { label: 'Toggle', to: '/components/Toggle' }] },
|
{ label: 'Elements', links: [{ label: 'Avatar', to: '/components/Avatar' }, { label: 'AvatarGroup', to: '/components/AvatarGroup' }, { label: 'Badge', to: '/components/Badge' }, { label: 'Button', to: '/components/Button' }, { label: 'Dropdown', to: '/components/Dropdown' }, { label: 'Icon', to: '/components/Icon' }, { label: 'Link', to: '/components/Link' }, { label: 'Toggle', to: '/components/Toggle' }] },
|
||||||
{ label: 'Feedback', links: [{ label: 'Alert', to: '/components/Alert' }] },
|
{ label: 'Feedback', links: [{ label: 'Alert', to: '/components/Alert' }] },
|
||||||
{ label: 'Forms', links: [{ label: 'Checkbox', to: '/components/Checkbox' }, { label: 'Input', to: '/components/Input' }, { label: 'InputGroup', to: '/components/InputGroup' }, { label: 'Radio', to: '/components/Radio' }, { label: 'RadioGroup', to: '/components/RadioGroup' }, { label: 'Select', to: '/components/Select' }, { label: 'SelectCustom', to: '/components/SelectCustom' }, { label: 'Textarea', to: '/components/Textarea' }] },
|
{ label: 'Forms', links: [{ label: 'Checkbox', to: '/components/Checkbox' }, { label: 'Input', to: '/components/Input' }, { label: 'InputGroup', to: '/components/InputGroup' }, { label: 'Radio', to: '/components/Radio' }, { label: 'RadioGroup', to: '/components/RadioGroup' }, { label: 'Select', to: '/components/Select' }, { label: 'SelectCustom', to: '/components/SelectCustom' }, { label: 'Textarea', to: '/components/Textarea' }] },
|
||||||
|
|||||||
@@ -54,6 +54,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import $ui from '#build/ui'
|
||||||
|
|
||||||
const nuxtApp = useNuxtApp()
|
const nuxtApp = useNuxtApp()
|
||||||
const { params } = useRoute()
|
const { params } = useRoute()
|
||||||
|
|
||||||
@@ -74,8 +76,15 @@ const refProps = Object.entries(componentProps).map(([key, prop]) => {
|
|||||||
|
|
||||||
let values
|
let values
|
||||||
if (prop.validator) {
|
if (prop.validator) {
|
||||||
const result = prop.validator.toString().match(/\[.*\]/g, '')[0]
|
const arrayRegex = prop.validator.toString().match(/\[.*\]/g, '')
|
||||||
values = JSON.parse(result.replace(/'/g, '"')).filter(Boolean)
|
if (arrayRegex) {
|
||||||
|
values = JSON.parse(arrayRegex[0].replace(/'/g, '"')).filter(Boolean)
|
||||||
|
} else {
|
||||||
|
const $uiProp = $ui[params.component.toLowerCase()][key]
|
||||||
|
if ($uiProp) {
|
||||||
|
values = Object.keys($uiProp).filter(Boolean)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ export default defineNuxtConfig({
|
|||||||
Options
|
Options
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
<p>- `preset`</p>
|
||||||
|
|
||||||
|
<p>Choose preset. Defaults to `tailwindui`. An object can also be used to override some parts of the default preset.</p>
|
||||||
|
|
||||||
<p>- `prefix`</p>
|
<p>- `prefix`</p>
|
||||||
|
|
||||||
<p>Define the prefix of the imported components. Defaults to `u`.</p>
|
<p>Define the prefix of the imported components. Defaults to `u`.</p>
|
||||||
|
|||||||
@@ -20,7 +20,10 @@
|
|||||||
Component
|
Component
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium u-text-gray-500 uppercase tracking-wider">
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium u-text-gray-500 uppercase tracking-wider">
|
||||||
Ready?
|
Composition API
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium u-text-gray-500 uppercase tracking-wider">
|
||||||
|
Preset system
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -32,7 +35,11 @@
|
|||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm u-text-gray-500">
|
<td class="px-6 py-4 whitespace-nowrap text-sm u-text-gray-500">
|
||||||
<span v-if="component.ready">✅</span>
|
<span v-if="component.capi">✅</span>
|
||||||
|
<span v-else>❌</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm u-text-gray-500">
|
||||||
|
<span v-if="component.preset">✅</span>
|
||||||
<span v-else>❌</span>
|
<span v-else>❌</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -63,12 +70,12 @@ const components = [
|
|||||||
{
|
{
|
||||||
label: 'Dropdown',
|
label: 'Dropdown',
|
||||||
to: '/components/Dropdown',
|
to: '/components/Dropdown',
|
||||||
ready: true
|
capi: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Icon',
|
label: 'Icon',
|
||||||
to: '/components/Icon',
|
to: '/components/Icon',
|
||||||
ready: true
|
capi: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Link',
|
label: 'Link',
|
||||||
@@ -89,7 +96,8 @@ const components = [
|
|||||||
{
|
{
|
||||||
label: 'Input',
|
label: 'Input',
|
||||||
to: '/components/Input',
|
to: '/components/Input',
|
||||||
ready: true
|
capi: true,
|
||||||
|
preset: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'InputGroup',
|
label: 'InputGroup',
|
||||||
@@ -105,7 +113,9 @@ const components = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Select',
|
label: 'Select',
|
||||||
to: '/components/Select'
|
to: '/components/Select',
|
||||||
|
capi: true,
|
||||||
|
preset: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'SelectCustom',
|
label: 'SelectCustom',
|
||||||
@@ -114,17 +124,18 @@ const components = [
|
|||||||
{
|
{
|
||||||
label: 'Textarea',
|
label: 'Textarea',
|
||||||
to: '/components/Textarea',
|
to: '/components/Textarea',
|
||||||
ready: true
|
capi: true,
|
||||||
|
preset: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Card',
|
label: 'Card',
|
||||||
to: '/components/Card',
|
to: '/components/Card',
|
||||||
ready: true
|
capi: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Container',
|
label: 'Container',
|
||||||
to: '/components/Container',
|
to: '/components/Container',
|
||||||
ready: true
|
capi: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Pills',
|
label: 'Pills',
|
||||||
@@ -141,7 +152,7 @@ const components = [
|
|||||||
{
|
{
|
||||||
label: 'Modal',
|
label: 'Modal',
|
||||||
to: '/components/Modal',
|
to: '/components/Modal',
|
||||||
ready: true
|
capi: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Notification',
|
label: 'Notification',
|
||||||
@@ -150,7 +161,7 @@ const components = [
|
|||||||
{
|
{
|
||||||
label: 'Popover',
|
label: 'Popover',
|
||||||
to: '/components/Popover',
|
to: '/components/Popover',
|
||||||
ready: true
|
capi: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Slideover',
|
label: 'Slideover',
|
||||||
|
|||||||
@@ -28,13 +28,15 @@
|
|||||||
"@iconify-json/heroicons-solid": "^1.0.2",
|
"@iconify-json/heroicons-solid": "^1.0.2",
|
||||||
"@popperjs/core": "^2.10.2",
|
"@popperjs/core": "^2.10.2",
|
||||||
"@unocss/nuxt": "^0.12.9",
|
"@unocss/nuxt": "^0.12.9",
|
||||||
|
"defu": "^5.0.0",
|
||||||
"gradient-avatar": "^1.0.2",
|
"gradient-avatar": "^1.0.2",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"pathe": "^0.2.0"
|
"pathe": "^0.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vueuse/core": "^7.1.1",
|
|
||||||
"@vueuse/components": "^7.1.1",
|
|
||||||
"@nuxtjs/eslint-config-typescript": "7.0.2",
|
"@nuxtjs/eslint-config-typescript": "7.0.2",
|
||||||
|
"@vueuse/components": "^7.1.1",
|
||||||
|
"@vueuse/core": "^7.1.1",
|
||||||
"eslint": "8.3.0",
|
"eslint": "8.3.0",
|
||||||
"nuxt3": "3.0.0-27296423.f3082ca",
|
"nuxt3": "3.0.0-27296423.f3082ca",
|
||||||
"unbuild": "0.5.13"
|
"unbuild": "0.5.13"
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="wrapperClass">
|
<div :class="wrapperClass">
|
||||||
<div
|
<div v-if="isLeading" :class="iconLeadingWrapperClass">
|
||||||
v-if="isLeading"
|
<Icon :name="iconName" :class="iconClass" />
|
||||||
class="absolute inset-y-0 left-0 flex items-center pointer-events-none"
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
:name="iconName"
|
|
||||||
class="u-text-gray-400"
|
|
||||||
:class="iconClass"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
:id="name"
|
:id="name"
|
||||||
@@ -22,21 +15,14 @@
|
|||||||
:readonly="readonly"
|
:readonly="readonly"
|
||||||
:autocomplete="autocomplete"
|
:autocomplete="autocomplete"
|
||||||
:spellcheck="spellcheck"
|
:spellcheck="spellcheck"
|
||||||
:class="[baseClass, sizeClass, paddingClass, paddingIconClass, appearanceClass, customClass]"
|
:class="inputClass"
|
||||||
@input="onInput($event.target.value)"
|
@input="onInput($event.target.value)"
|
||||||
@focus="$emit('focus', $event)"
|
@focus="$emit('focus', $event)"
|
||||||
@blur="$emit('blur', $event)"
|
@blur="$emit('blur', $event)"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
<div
|
<div v-if="isTrailing" :class="iconTrailingWrapperClass">
|
||||||
v-if="isTrailing"
|
<Icon :name="iconName" :class="iconClass" />
|
||||||
class="absolute inset-y-0 right-0 flex items-center pointer-events-none"
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
:name="iconName"
|
|
||||||
class="u-text-gray-400"
|
|
||||||
:class="iconClass"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -44,6 +30,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import Icon from '../elements/Icon'
|
import Icon from '../elements/Icon'
|
||||||
|
import { classNames } from '../../utils'
|
||||||
|
import $ui from '#build/ui'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -96,7 +84,7 @@ export default {
|
|||||||
},
|
},
|
||||||
loadingIcon: {
|
loadingIcon: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: 'heroicons-outline:refresh'
|
||||||
},
|
},
|
||||||
trailing: {
|
trailing: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -110,16 +98,20 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'md',
|
default: 'md',
|
||||||
validator (value) {
|
validator (value) {
|
||||||
return ['', 'xxs', 'xs', 'sm', 'md', 'lg', 'xl'].includes(value)
|
return Object.keys($ui.input.size).includes(value)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
wrapperClass: {
|
wrapperClass: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'relative'
|
default: $ui.input.wrapper
|
||||||
},
|
},
|
||||||
baseClass: {
|
baseClass: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'block w-full u-bg-white u-text-gray-700 disabled:cursor-not-allowed disabled:u-bg-gray-50 focus:outline-none'
|
default: () => $ui.input.base
|
||||||
|
},
|
||||||
|
iconBaseClass: {
|
||||||
|
type: String,
|
||||||
|
default: () => $ui.input.icon.base
|
||||||
},
|
},
|
||||||
customClass: {
|
customClass: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -129,7 +121,7 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'default',
|
default: 'default',
|
||||||
validator (value) {
|
validator (value) {
|
||||||
return ['default', 'none'].includes(value)
|
return Object.keys($ui.input.appearance).includes(value)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
loading: {
|
loading: {
|
||||||
@@ -157,50 +149,6 @@ export default {
|
|||||||
}, 100)
|
}, 100)
|
||||||
})
|
})
|
||||||
|
|
||||||
const sizeClass = computed(() => ({
|
|
||||||
xxs: 'text-xs',
|
|
||||||
xs: 'text-xs',
|
|
||||||
sm: 'text-sm leading-4',
|
|
||||||
md: 'text-sm',
|
|
||||||
lg: 'text-base',
|
|
||||||
xl: 'text-base'
|
|
||||||
})[props.size])
|
|
||||||
|
|
||||||
const paddingClass = computed(() => ({
|
|
||||||
xxs: 'px-1 py-0.5',
|
|
||||||
xs: 'px-2.5 py-1.5',
|
|
||||||
sm: 'px-3 py-2',
|
|
||||||
md: 'px-4 py-2',
|
|
||||||
lg: 'px-4 py-2',
|
|
||||||
xl: 'px-6 py-3'
|
|
||||||
})[props.size])
|
|
||||||
|
|
||||||
const appearanceClass = computed(() => ({
|
|
||||||
default: 'focus:ring-1 focus:ring-primary-500 focus:border-primary-500 border u-border-gray-300 rounded-md shadow-sm',
|
|
||||||
none: 'border-0 bg-transparent focus:ring-0 focus:shadow-none'
|
|
||||||
})[props.appearance])
|
|
||||||
|
|
||||||
const paddingIconClass = computed(() => {
|
|
||||||
return [
|
|
||||||
props.isLeading && ({
|
|
||||||
xxs: 'pl-7',
|
|
||||||
xs: 'pl-7',
|
|
||||||
sm: 'pl-10',
|
|
||||||
md: 'pl-10',
|
|
||||||
lg: 'pl-10',
|
|
||||||
xl: 'pl-10'
|
|
||||||
})[props.size],
|
|
||||||
props.isTrailing && ({
|
|
||||||
xxs: 'pr-10',
|
|
||||||
xs: 'pr-10',
|
|
||||||
sm: 'pr-10',
|
|
||||||
md: 'pr-10',
|
|
||||||
lg: 'pr-10',
|
|
||||||
xl: 'pr-10'
|
|
||||||
})[props.size]
|
|
||||||
].join(' ')
|
|
||||||
})
|
|
||||||
|
|
||||||
const isLeading = computed(() => {
|
const isLeading = computed(() => {
|
||||||
return (props.icon && props.leading) || (props.icon && !props.trailing) || (props.loading && !props.trailing)
|
return (props.icon && props.leading) || (props.icon && !props.trailing) || (props.loading && !props.trailing)
|
||||||
})
|
})
|
||||||
@@ -209,55 +157,47 @@ export default {
|
|||||||
return (props.icon && props.trailing) || (props.loading && props.trailing)
|
return (props.icon && props.trailing) || (props.loading && props.trailing)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const inputClass = computed(() => {
|
||||||
|
return classNames(
|
||||||
|
props.baseClass,
|
||||||
|
$ui.input.size[props.size],
|
||||||
|
$ui.input.spacing[props.size],
|
||||||
|
$ui.input.appearance[props.appearance],
|
||||||
|
isLeading.value && $ui.input.leading.spacing[props.size],
|
||||||
|
isTrailing.value && $ui.input.trailing.spacing[props.size],
|
||||||
|
props.customClass
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
const iconName = computed(() => {
|
const iconName = computed(() => {
|
||||||
if (props.loading) {
|
if (props.loading) {
|
||||||
return props.loadingIcon || 'custom/loading'
|
return props.loadingIcon
|
||||||
}
|
}
|
||||||
|
|
||||||
return props.icon
|
return props.icon
|
||||||
})
|
})
|
||||||
|
|
||||||
const iconClass = computed(() => {
|
const iconClass = computed(() => {
|
||||||
return [
|
return classNames(
|
||||||
({
|
props.iconBaseClass,
|
||||||
xxs: 'h-3 w-3',
|
$ui.input.icon.size[props.size],
|
||||||
xs: 'h-4 w-4',
|
isLeading.value && $ui.input.icon.leading.spacing[props.size],
|
||||||
sm: 'h-5 w-5',
|
isTrailing.value && $ui.input.icon.trailing.spacing[props.size],
|
||||||
md: 'h-5 w-5',
|
props.loading && 'animate-spin'
|
||||||
lg: 'h-5 w-5',
|
)
|
||||||
xl: 'h-5 w-5'
|
|
||||||
})[props.size || 'sm'],
|
|
||||||
props.isLeading && ({
|
|
||||||
xxs: 'ml-2',
|
|
||||||
xs: 'ml-2',
|
|
||||||
sm: 'ml-3',
|
|
||||||
md: 'ml-3',
|
|
||||||
lg: 'ml-3',
|
|
||||||
xl: 'ml-3'
|
|
||||||
})[props.size || 'sm'],
|
|
||||||
props.isTrailing && ({
|
|
||||||
xxs: 'mr-2',
|
|
||||||
xs: 'mr-2',
|
|
||||||
sm: 'mr-3',
|
|
||||||
md: 'mr-3',
|
|
||||||
lg: 'mr-3',
|
|
||||||
xl: 'mr-3'
|
|
||||||
})[props.size || 'sm'],
|
|
||||||
({
|
|
||||||
true: 'animate-spin'
|
|
||||||
})[props.loading]
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const iconLeadingWrapperClass = $ui.input.icon.leading.wrapper
|
||||||
|
const iconTrailingWrapperClass = $ui.input.icon.trailing.wrapper
|
||||||
|
|
||||||
return {
|
return {
|
||||||
input,
|
input,
|
||||||
onInput,
|
onInput,
|
||||||
sizeClass,
|
inputClass,
|
||||||
paddingClass,
|
|
||||||
paddingIconClass,
|
|
||||||
appearanceClass,
|
|
||||||
iconClass,
|
|
||||||
iconName,
|
iconName,
|
||||||
|
iconClass,
|
||||||
|
iconLeadingWrapperClass,
|
||||||
|
iconTrailingWrapperClass,
|
||||||
isLeading,
|
isLeading,
|
||||||
isTrailing
|
isTrailing
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="wrapperClass">
|
<div :class="wrapperClass">
|
||||||
|
<div v-if="icon" :class="iconWrapperClass">
|
||||||
|
<Icon :name="icon" :class="iconClass" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<select
|
<select
|
||||||
:id="name"
|
:id="name"
|
||||||
:name="name"
|
:name="name"
|
||||||
:required="required"
|
:required="required"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:readonly="readonly"
|
|
||||||
:class="selectClass"
|
:class="selectClass"
|
||||||
@input="updateValue($event.target.value)"
|
@input="onInput($event.target.value)"
|
||||||
>
|
>
|
||||||
<template v-for="(option, index) in normalizedOptionsWithPlaceholder">
|
<template v-for="(option, index) in normalizedOptionsWithPlaceholder">
|
||||||
<optgroup
|
<optgroup
|
||||||
@@ -33,16 +36,14 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</select>
|
</select>
|
||||||
<div v-if="icon" class="absolute inset-y-0 left-0 flex items-center pointer-events-none" :class="iconPadding">
|
|
||||||
<Icon :name="icon" :class="iconClass" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import get from 'lodash/get'
|
import get from 'lodash/get'
|
||||||
|
|
||||||
import Icon from '../elements/Icon'
|
import Icon from '../elements/Icon'
|
||||||
|
import { classNames } from '../../utils'
|
||||||
|
import $ui from '#build/ui'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -73,24 +74,24 @@ export default {
|
|||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
readonly: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
size: {
|
size: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'md',
|
default: 'md',
|
||||||
validator (value) {
|
validator (value) {
|
||||||
return ['xxs', 'xs', 'sm', 'md', 'lg', 'xl'].includes(value)
|
return Object.keys($ui.select.size).includes(value)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
wrapperClass: {
|
wrapperClass: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'relative'
|
default: $ui.select.wrapper
|
||||||
},
|
},
|
||||||
baseClass: {
|
baseClass: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'block w-full disabled:cursor-not-allowed u-bg-white u-text-gray-700 disabled:u-bg-gray-50 focus:ring-1 focus:ring-primary-500 focus:border-primary-500 dark:focus:border-primary-500 border u-border-gray-300 rounded-md shadow-sm focus:outline-none'
|
default: $ui.select.base
|
||||||
|
},
|
||||||
|
iconBaseClass: {
|
||||||
|
type: String,
|
||||||
|
default: $ui.select.icon.base
|
||||||
},
|
},
|
||||||
customClass: {
|
customClass: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -110,101 +111,97 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['update:modelValue'],
|
emits: ['update:modelValue'],
|
||||||
computed: {
|
setup (props, { emit }) {
|
||||||
sizeClass () {
|
const select = ref(null)
|
||||||
return {
|
|
||||||
xxs: 'text-xs',
|
|
||||||
xs: 'text-xs',
|
|
||||||
sm: 'text-sm leading-4',
|
|
||||||
md: 'text-sm',
|
|
||||||
lg: 'text-base',
|
|
||||||
xl: 'text-base'
|
|
||||||
}[this.size]
|
|
||||||
},
|
|
||||||
paddingClass () {
|
|
||||||
return ({
|
|
||||||
xxs: `${this.icon ? 'pl-7' : 'pl-2'} pr-7 py-1.5`,
|
|
||||||
xs: `${this.icon ? 'pl-8' : 'pl-3'} pr-9 py-1.5`,
|
|
||||||
sm: `${this.icon ? 'pl-8' : 'pl-3'} pr-9 py-2`,
|
|
||||||
md: `${this.icon ? 'pl-10' : 'pl-3'} pr-10 py-2`,
|
|
||||||
lg: `${this.icon ? 'pl-10' : 'pl-3'} pr-10 py-2`,
|
|
||||||
xl: `${this.icon ? 'pl-12' : 'pl-4'} pr-12 py-3`
|
|
||||||
})[this.size]
|
|
||||||
},
|
|
||||||
iconClass () {
|
|
||||||
return ({
|
|
||||||
xxs: 'w-3 h-3',
|
|
||||||
xs: 'w-4 h-4',
|
|
||||||
sm: 'w-4 h-4',
|
|
||||||
md: 'w-5 h-5',
|
|
||||||
lg: 'w-5 h-5',
|
|
||||||
xl: 'w-5 h-5'
|
|
||||||
})[this.size]
|
|
||||||
},
|
|
||||||
iconPadding () {
|
|
||||||
return ({
|
|
||||||
xxs: 'pl-3',
|
|
||||||
xs: 'pl-3',
|
|
||||||
sm: 'pl-3',
|
|
||||||
md: 'pl-3',
|
|
||||||
lg: 'pl-3',
|
|
||||||
xl: 'pl-4'
|
|
||||||
})[this.size]
|
|
||||||
},
|
|
||||||
selectClass () {
|
|
||||||
return [
|
|
||||||
this.baseClass,
|
|
||||||
this.customClass,
|
|
||||||
this.sizeClass,
|
|
||||||
this.paddingClass
|
|
||||||
].join(' ')
|
|
||||||
},
|
|
||||||
normalizedOptions () {
|
|
||||||
return this.options.map(option => this.normalizeOption(option))
|
|
||||||
},
|
|
||||||
normalizedOptionsWithPlaceholder () {
|
|
||||||
if (!this.placeholder) {
|
|
||||||
return this.normalizedOptions
|
|
||||||
}
|
|
||||||
const { normalizedOptions } = this
|
|
||||||
normalizedOptions.unshift({
|
|
||||||
[this.valueAttribute]: null,
|
|
||||||
[this.textAttribute]: this.placeholder
|
|
||||||
})
|
|
||||||
return normalizedOptions
|
|
||||||
},
|
|
||||||
normalizedValue () {
|
|
||||||
const foundOption = this.normalizedOptionsWithPlaceholder.find(option => option.value === this.modelValue)
|
|
||||||
if (!foundOption) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return foundOption.value
|
const onInput = (value) => {
|
||||||
|
emit('update:modelValue', value)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
methods: {
|
const guessOptionValue = (option) => {
|
||||||
guessOptionValue (option) {
|
return get(option, props.valueAttribute, get(option, props.textAttribute))
|
||||||
return get(option, this.valueAttribute, get(option, this.textAttribute))
|
}
|
||||||
},
|
|
||||||
guessOptionText (option) {
|
const guessOptionText = (option) => {
|
||||||
return get(option, this.textAttribute, get(option, this.valueAttribute))
|
return get(option, props.textAttribute, get(option, props.valueAttribute))
|
||||||
},
|
}
|
||||||
normalizeOption (option) {
|
|
||||||
|
const normalizeOption = (option) => {
|
||||||
if (['string', 'number', 'boolean'].includes(typeof option)) {
|
if (['string', 'number', 'boolean'].includes(typeof option)) {
|
||||||
return {
|
return {
|
||||||
[this.valueAttribute]: option,
|
[props.valueAttribute]: option,
|
||||||
[this.textAttribute]: option
|
[props.textAttribute]: option
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...option,
|
...option,
|
||||||
[this.valueAttribute]: this.guessOptionValue(option),
|
[props.valueAttribute]: guessOptionValue(option),
|
||||||
[this.textAttribute]: this.guessOptionText(option)
|
[props.textAttribute]: guessOptionText(option)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
updateValue (value) {
|
|
||||||
this.$emit('update:modelValue', value)
|
const normalizedOptions = computed(() => {
|
||||||
|
return props.options.map(option => normalizeOption(option))
|
||||||
|
})
|
||||||
|
|
||||||
|
const normalizedOptionsWithPlaceholder = computed(() => {
|
||||||
|
if (!props.placeholder) {
|
||||||
|
return normalizedOptions.value
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
[props.valueAttribute]: null,
|
||||||
|
[props.textAttribute]: props.placeholder
|
||||||
|
},
|
||||||
|
...normalizedOptions.value
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const normalizedValue = computed(() => {
|
||||||
|
const foundOption = normalizedOptionsWithPlaceholder.value.find(option => option.value === props.modelValue)
|
||||||
|
if (!foundOption) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return foundOption.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectClass = computed(() => {
|
||||||
|
return classNames(
|
||||||
|
props.baseClass,
|
||||||
|
$ui.select.size[props.size],
|
||||||
|
$ui.select.spacing[props.size],
|
||||||
|
$ui.select.appearance.default,
|
||||||
|
!!props.icon && $ui.select.leading.spacing[props.size],
|
||||||
|
$ui.select.trailing.spacing[props.size],
|
||||||
|
props.customClass
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const iconClass = computed(() => {
|
||||||
|
return classNames(
|
||||||
|
props.iconBaseClass,
|
||||||
|
$ui.select.icon.size[props.size],
|
||||||
|
!!props.icon && $ui.select.icon.leading.spacing[props.size]
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const iconWrapperClass = $ui.select.icon.leading.base
|
||||||
|
|
||||||
|
return {
|
||||||
|
select,
|
||||||
|
onInput,
|
||||||
|
guessOptionValue,
|
||||||
|
guessOptionText,
|
||||||
|
normalizeOption,
|
||||||
|
normalizedOptions,
|
||||||
|
normalizedOptionsWithPlaceholder,
|
||||||
|
normalizedValue,
|
||||||
|
selectClass,
|
||||||
|
iconClass,
|
||||||
|
iconWrapperClass
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:autocomplete="autocomplete"
|
:autocomplete="autocomplete"
|
||||||
:class="[baseClass, customClass, sizeClass, paddingClass, appearanceClass, resizeClass]"
|
:class="textareaClass"
|
||||||
@input="onInput($event.target.value)"
|
@input="onInput($event.target.value)"
|
||||||
@focus="$emit('focus', $event)"
|
@focus="$emit('focus', $event)"
|
||||||
@blur="$emit('blur', $event)"
|
@blur="$emit('blur', $event)"
|
||||||
@@ -20,6 +20,8 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { classNames } from '../../utils'
|
||||||
|
import $ui from '#build/ui'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
@@ -63,7 +65,7 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'default',
|
default: 'default',
|
||||||
validator (value) {
|
validator (value) {
|
||||||
return ['default', 'none'].includes(value)
|
return Object.keys($ui.textarea.appearance).includes(value)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resize: {
|
resize: {
|
||||||
@@ -74,16 +76,16 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'md',
|
default: 'md',
|
||||||
validator (value) {
|
validator (value) {
|
||||||
return ['', 'xxs', 'xs', 'sm', 'md', 'lg', 'xl'].includes(value)
|
return Object.keys($ui.textarea.size).includes(value)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
wrapperClass: {
|
wrapperClass: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'relative'
|
default: $ui.textarea.wrapper
|
||||||
},
|
},
|
||||||
baseClass: {
|
baseClass: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'block w-full u-bg-white u-text-gray-700 disabled:cursor-not-allowed disabled:u-bg-gray-50 focus:outline-none'
|
default: $ui.textarea.base
|
||||||
},
|
},
|
||||||
customClass: {
|
customClass: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -127,40 +129,21 @@ export default {
|
|||||||
}, 100)
|
}, 100)
|
||||||
})
|
})
|
||||||
|
|
||||||
const sizeClass = computed(() => ({
|
const textareaClass = computed(() => {
|
||||||
xxs: 'text-xs',
|
return classNames(
|
||||||
xs: 'text-xs',
|
props.baseClass,
|
||||||
sm: 'text-sm leading-4',
|
$ui.textarea.size[props.size],
|
||||||
md: 'text-sm',
|
$ui.textarea.spacing[props.size],
|
||||||
lg: 'text-base',
|
$ui.textarea.appearance[props.appearance],
|
||||||
xl: 'text-base'
|
!props.resize && 'resize-none',
|
||||||
})[props.size])
|
props.customClass
|
||||||
|
)
|
||||||
const paddingClass = computed(() => ({
|
|
||||||
xxs: 'px-1 py-0.5',
|
|
||||||
xs: 'px-2.5 py-1.5',
|
|
||||||
sm: 'px-3 py-2',
|
|
||||||
md: 'px-4 py-2',
|
|
||||||
lg: 'px-4 py-2',
|
|
||||||
xl: 'px-6 py-3'
|
|
||||||
})[props.size])
|
|
||||||
|
|
||||||
const appearanceClass = computed(() => ({
|
|
||||||
default: 'focus:ring-1 focus:ring-primary-500 focus:border-primary-500 border u-border-gray-300 rounded-md shadow-sm',
|
|
||||||
none: 'border-0 bg-transparent focus:ring-0 focus:shadow-none'
|
|
||||||
})[props.appearance])
|
|
||||||
|
|
||||||
const resizeClass = computed(() => {
|
|
||||||
return props.resize ? '' : 'resize-none'
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
textarea,
|
textarea,
|
||||||
onInput,
|
onInput,
|
||||||
sizeClass,
|
textareaClass
|
||||||
paddingClass,
|
|
||||||
appearanceClass,
|
|
||||||
resizeClass
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
75
src/index.ts
75
src/index.ts
@@ -1,6 +1,7 @@
|
|||||||
import { resolve } from 'pathe'
|
import { resolve } from 'pathe'
|
||||||
import { defineNuxtModule, installModule, addComponentsDir } from '@nuxt/kit'
|
import { defineNuxtModule, installModule, addComponentsDir, addTemplate } from '@nuxt/kit'
|
||||||
import { colors } from '@unocss/preset-uno'
|
import { colors } from '@unocss/preset-uno'
|
||||||
|
import defu from 'defu'
|
||||||
import type { UnocssNuxtOptions } from '@unocss/nuxt'
|
import type { UnocssNuxtOptions } from '@unocss/nuxt'
|
||||||
|
|
||||||
export interface UiColorsOptions {
|
export interface UiColorsOptions {
|
||||||
@@ -17,8 +18,11 @@ export interface UiColorsOptions {
|
|||||||
|
|
||||||
export interface UiOptions {
|
export interface UiOptions {
|
||||||
/**
|
/**
|
||||||
* Prefix of injected components.
|
* @default 'tailwindui'
|
||||||
*
|
*/
|
||||||
|
preset?: string | object
|
||||||
|
|
||||||
|
/**
|
||||||
* @default 'u'
|
* @default 'u'
|
||||||
*/
|
*/
|
||||||
prefix?: string
|
prefix?: string
|
||||||
@@ -28,24 +32,27 @@ export interface UiOptions {
|
|||||||
unocss?: UnocssNuxtOptions
|
unocss?: UnocssNuxtOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaults = {
|
||||||
|
preset: 'tailwindui',
|
||||||
|
prefix: 'u',
|
||||||
|
colors: {
|
||||||
|
primary: 'indigo',
|
||||||
|
gray: 'zinc'
|
||||||
|
},
|
||||||
|
unocss: {
|
||||||
|
shortcuts: [],
|
||||||
|
rules: [],
|
||||||
|
variants: [],
|
||||||
|
theme: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default defineNuxtModule<UiOptions>({
|
export default defineNuxtModule<UiOptions>({
|
||||||
name: '@nuxthq/ui',
|
name: '@nuxthq/ui',
|
||||||
configKey: 'ui',
|
configKey: 'ui',
|
||||||
defaults: {
|
defaults,
|
||||||
prefix: 'u',
|
|
||||||
colors: {
|
|
||||||
primary: 'indigo',
|
|
||||||
gray: 'zinc'
|
|
||||||
},
|
|
||||||
unocss: {
|
|
||||||
shortcuts: [],
|
|
||||||
rules: [],
|
|
||||||
variants: [],
|
|
||||||
theme: {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async setup (_options, nuxt) {
|
async setup (_options, nuxt) {
|
||||||
const { prefix, colors: { primary = 'indigo', gray = 'zinc' } = {} } = _options
|
const { preset, prefix, colors: { primary = 'indigo', gray = 'zinc' } = {} } = _options
|
||||||
const { shortcuts = [], rules = [], variants = [], theme = {} } = _options.unocss || {}
|
const { shortcuts = [], rules = [], variants = [], theme = {} } = _options.unocss || {}
|
||||||
|
|
||||||
const options: UnocssNuxtOptions = {
|
const options: UnocssNuxtOptions = {
|
||||||
@@ -134,7 +141,18 @@ export default defineNuxtModule<UiOptions>({
|
|||||||
}],
|
}],
|
||||||
...rules
|
...rules
|
||||||
],
|
],
|
||||||
variants,
|
variants: [
|
||||||
|
// disabled:
|
||||||
|
(matcher) => {
|
||||||
|
if (!matcher.startsWith('disabled:')) { return matcher }
|
||||||
|
return {
|
||||||
|
// slice `disabled:` prefix and passed to the next variants and rules
|
||||||
|
matcher: matcher.slice(9),
|
||||||
|
selector: s => `${s}:disabled`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...variants
|
||||||
|
],
|
||||||
layers: {
|
layers: {
|
||||||
icons: 0,
|
icons: 0,
|
||||||
default: 1,
|
default: 1,
|
||||||
@@ -144,6 +162,24 @@ export default defineNuxtModule<UiOptions>({
|
|||||||
|
|
||||||
await installModule(nuxt, { src: '@unocss/nuxt', options })
|
await installModule(nuxt, { src: '@unocss/nuxt', options })
|
||||||
|
|
||||||
|
let ui: object = {}
|
||||||
|
try {
|
||||||
|
if (typeof preset === 'object') {
|
||||||
|
ui = await import(resolve(__dirname, `./presets/${defaults.preset}`))
|
||||||
|
|
||||||
|
ui = defu(preset, ui)
|
||||||
|
} else {
|
||||||
|
ui = await import(resolve(__dirname, `./presets/${preset}`))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ui = await import(resolve(__dirname, `./presets/${defaults.preset}`))
|
||||||
|
}
|
||||||
|
|
||||||
|
addTemplate({
|
||||||
|
filename: 'ui.mjs',
|
||||||
|
getContents: () => `/* @unocss-include */ export default ${JSON.stringify(ui)}`
|
||||||
|
})
|
||||||
|
|
||||||
addComponentsDir({
|
addComponentsDir({
|
||||||
path: resolve(__dirname, './components/elements'),
|
path: resolve(__dirname, './components/elements'),
|
||||||
prefix,
|
prefix,
|
||||||
@@ -183,4 +219,7 @@ declare module '@nuxt/schema' {
|
|||||||
interface NuxtConfig {
|
interface NuxtConfig {
|
||||||
ui?: UiOptions
|
ui?: UiOptions
|
||||||
}
|
}
|
||||||
|
interface NuxtOptions {
|
||||||
|
ui?: UiOptions
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
96
src/presets/tailwindui.ts
Normal file
96
src/presets/tailwindui.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
const input = {
|
||||||
|
wrapper: 'relative',
|
||||||
|
base: 'block w-full u-bg-white u-text-gray-700 disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none',
|
||||||
|
size: {
|
||||||
|
xxs: 'text-xs',
|
||||||
|
xs: 'text-xs',
|
||||||
|
sm: 'text-sm leading-4',
|
||||||
|
md: 'text-sm',
|
||||||
|
lg: 'text-base',
|
||||||
|
xl: 'text-base'
|
||||||
|
},
|
||||||
|
spacing: {
|
||||||
|
xxs: 'px-1 py-0.5',
|
||||||
|
xs: 'px-2.5 py-1.5',
|
||||||
|
sm: 'px-3 py-2',
|
||||||
|
md: 'px-4 py-2',
|
||||||
|
lg: 'px-4 py-2',
|
||||||
|
xl: 'px-6 py-3'
|
||||||
|
},
|
||||||
|
leading: {
|
||||||
|
spacing: {
|
||||||
|
xxs: 'pl-7',
|
||||||
|
xs: 'pl-7',
|
||||||
|
sm: 'pl-10',
|
||||||
|
md: 'pl-10',
|
||||||
|
lg: 'pl-10',
|
||||||
|
xl: 'pl-10'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trailing: {
|
||||||
|
spacing: {
|
||||||
|
xxs: 'pr-7',
|
||||||
|
xs: 'pr-7',
|
||||||
|
sm: 'pr-10',
|
||||||
|
md: 'pr-10',
|
||||||
|
lg: 'pr-10',
|
||||||
|
xl: 'pr-10'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
appearance: {
|
||||||
|
default: 'focus:ring-1 focus:ring-primary-500 focus:border-primary-500 border u-border-gray-300 rounded-md shadow-sm',
|
||||||
|
none: 'border-0 bg-transparent focus:ring-0 focus:shadow-none'
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
base: 'u-text-gray-400',
|
||||||
|
size: {
|
||||||
|
xxs: 'h-3 w-3',
|
||||||
|
xs: 'h-4 w-4',
|
||||||
|
sm: 'h-5 w-5',
|
||||||
|
md: 'h-5 w-5',
|
||||||
|
lg: 'h-5 w-5',
|
||||||
|
xl: 'h-5 w-5'
|
||||||
|
},
|
||||||
|
leading: {
|
||||||
|
wrapper: 'absolute inset-y-0 left-0 flex items-center pointer-events-none',
|
||||||
|
spacing: {
|
||||||
|
xxs: 'ml-2',
|
||||||
|
xs: 'ml-2',
|
||||||
|
sm: 'ml-3',
|
||||||
|
md: 'ml-3',
|
||||||
|
lg: 'ml-3',
|
||||||
|
xl: 'ml-3'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trailing: {
|
||||||
|
wrapper: 'absolute inset-y-0 right-0 flex items-center pointer-events-none',
|
||||||
|
spacing: {
|
||||||
|
xxs: 'mr-2',
|
||||||
|
xs: 'mr-2',
|
||||||
|
sm: 'mr-3',
|
||||||
|
md: 'mr-3',
|
||||||
|
lg: 'mr-3',
|
||||||
|
xl: 'mr-3'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const textarea = {
|
||||||
|
...input
|
||||||
|
}
|
||||||
|
|
||||||
|
const select = {
|
||||||
|
...input
|
||||||
|
}
|
||||||
|
|
||||||
|
const button = {
|
||||||
|
base: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
input,
|
||||||
|
textarea,
|
||||||
|
select,
|
||||||
|
button
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user