chore: migrate components to typescript setup (#55)

Co-authored-by: Benjamin Canac <canacb1@gmail.com>
This commit is contained in:
Sylvain Marroufin
2022-05-09 16:05:07 +02:00
committed by GitHub
parent 6c2f93f262
commit 39bf242f78
19 changed files with 1538 additions and 1684 deletions

View File

@@ -292,7 +292,9 @@ const dropdownItems = [
icon: 'heroicons-solid:archive' icon: 'heroicons-solid:archive'
}, { }, {
label: 'Move', label: 'Move',
icon: 'heroicons-solid:external-link' icon: 'heroicons-solid:external-link',
to: 'https://www.google.fr',
target: '_blank'
}], }],
[{ [{
label: 'Delete', label: 'Delete',

View File

@@ -76,7 +76,8 @@ const components = [
{ {
label: 'AvatarGroup', label: 'AvatarGroup',
to: '/components/AvatarGroup', to: '/components/AvatarGroup',
nuxt3: true nuxt3: true,
typescript: true
}, },
{ {
label: 'Badge', label: 'Badge',
@@ -91,14 +92,16 @@ const components = [
to: '/components/Button', to: '/components/Button',
nuxt3: true, nuxt3: true,
capi: true, capi: true,
preset: true preset: true,
typescript: true
}, },
{ {
label: 'Dropdown', label: 'Dropdown',
to: '/components/Dropdown', to: '/components/Dropdown',
nuxt3: true, nuxt3: true,
capi: true, capi: true,
preset: true preset: true,
typescript: true
}, },
{ {
label: 'Icon', label: 'Icon',
@@ -111,7 +114,8 @@ const components = [
label: 'Link', label: 'Link',
to: '/components/Link', to: '/components/Link',
nuxt3: true, nuxt3: true,
capi: true capi: true,
typescript: true
}, },
{ {
label: 'Toggle', label: 'Toggle',
@@ -124,7 +128,8 @@ const components = [
{ {
label: 'Alert', label: 'Alert',
to: '/components/Alert', to: '/components/Alert',
nuxt3: true nuxt3: true,
typescript: true
}, },
{ {
label: 'AlertDialog', label: 'AlertDialog',
@@ -138,35 +143,40 @@ const components = [
label: 'Input', label: 'Input',
to: '/components/Input', to: '/components/Input',
capi: true, capi: true,
preset: true preset: true,
typescript: true
}, },
{ {
label: 'FormGroup', label: 'FormGroup',
to: '/components/FormGroup', to: '/components/FormGroup',
nuxt3: true, nuxt3: true,
capi: true, capi: true,
preset: true preset: true,
typescript: true
}, },
{ {
label: 'Checkbox', label: 'Checkbox',
to: '/components/Checkbox', to: '/components/Checkbox',
nuxt3: true, nuxt3: true,
capi: true, capi: true,
preset: true preset: true,
typescript: true
}, },
{ {
label: 'Radio', label: 'Radio',
to: '/components/Radio', to: '/components/Radio',
nuxt3: true, nuxt3: true,
capi: true, capi: true,
preset: true preset: true,
typescript: true
}, },
{ {
label: 'Select', label: 'Select',
to: '/components/Select', to: '/components/Select',
nuxt3: true, nuxt3: true,
capi: true, capi: true,
preset: true preset: true,
typescript: true
}, },
{ {
label: 'SelectCustom', label: 'SelectCustom',
@@ -178,14 +188,16 @@ const components = [
to: '/components/Textarea', to: '/components/Textarea',
nuxt3: true, nuxt3: true,
capi: true, capi: true,
preset: true preset: true,
typescript: true
}, },
{ {
label: 'Card', label: 'Card',
to: '/components/Card', to: '/components/Card',
nuxt3: true, nuxt3: true,
capi: true, capi: true,
preset: true preset: true,
typescript: true
}, },
{ {
label: 'Container', label: 'Container',
@@ -216,20 +228,23 @@ const components = [
to: '/components/VerticalNavigation', to: '/components/VerticalNavigation',
nuxt3: true, nuxt3: true,
capi: true, capi: true,
preset: true preset: true,
typescript: true
}, },
{ {
label: 'Modal', label: 'Modal',
to: '/components/Modal', to: '/components/Modal',
nuxt3: true, nuxt3: true,
preset: true, preset: true,
capi: true capi: true,
typescript: true
}, },
{ {
label: 'Notification', label: 'Notification',
to: '/components/Notification', to: '/components/Notification',
nuxt3: true, nuxt3: true,
capi: true capi: true,
typescript: true
}, },
{ {
label: 'Notifications', label: 'Notifications',
@@ -242,7 +257,8 @@ const components = [
label: 'Popover', label: 'Popover',
to: '/components/Popover', to: '/components/Popover',
nuxt3: true, nuxt3: true,
capi: true capi: true,
typescript: true
}, },
{ {
label: 'Slideover', label: 'Slideover',
@@ -255,7 +271,8 @@ const components = [
label: 'Tooltip', label: 'Tooltip',
to: '/components/Tooltip', to: '/components/Tooltip',
nuxt3: true, nuxt3: true,
capi: true capi: true,
typescript: true
} }
] ]
</script> </script>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="flex"> <div class="flex">
<Avatar <Avatar
v-for="(avatar, index) of avatars" v-for="(avatar, index) of displayedGroup"
:key="index" :key="index"
v-bind="avatar" v-bind="avatar"
class="ring-2 u-ring-white -ml-1.5 first:ml-0" class="ring-2 u-ring-white -ml-1.5 first:ml-0"
@@ -16,46 +16,43 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { computed } from 'vue'
import Avatar from './Avatar' import Avatar from './Avatar'
export default { const props = defineProps({
components: { group: {
Avatar type: Array,
default: () => []
}, },
props: { size: {
group: { type: String,
type: Array, default: 'md',
default: () => [] validator (value: string) {
}, return ['xxxs', 'xxs', 'xs', 'sm', 'md', 'lg', 'xl'].includes(value)
size: {
type: String,
default: 'md',
validator (value) {
return ['xxxs', 'xxs', 'xs', 'sm', 'md', 'lg', 'xl'].includes(value)
}
},
max: {
type: Number,
default: null
} }
}, },
computed: { max: {
avatars () { type: Number,
return this.group.map((avatar) => { default: null
return typeof avatar === 'string' ? { src: avatar } : avatar
})
},
displayedGroup () {
if (!this.max) { return this.avatars }
return this.avatars.slice(0, this.max)
},
remainingGroupSize () {
if (!this.max) { return 0 }
return this.avatars.length - this.max
}
} }
} })
const avatars = computed(() => {
return props.group.map((avatar) => {
return typeof avatar === 'string' ? { src: avatar } : avatar
})
})
const displayedGroup = computed(() => {
if (!props.max) { return avatars.value }
return avatars.value.slice(0, props.max)
})
const remainingGroupSize = computed(() => {
if (!props.max) { return 0 }
return avatars.value.length - props.max
})
</script> </script>

View File

@@ -12,174 +12,159 @@
</component> </component>
</template> </template>
<script> <script setup lang="ts">
import { ref, computed } from 'vue' import { ref, computed, useSlots } from 'vue'
import Link from '../elements/Link'
import Icon from '../elements/Icon' import Icon from '../elements/Icon'
import { classNames } from '../../utils' import { classNames } from '../../utils'
import NuxtLink from '#app/components/nuxt-link'
import $ui from '#build/ui' import $ui from '#build/ui'
export default { const props = defineProps({
components: { type: {
Icon, type: String,
Link default: 'button'
}, },
props: { block: {
type: { type: Boolean,
type: String, default: false
default: 'button' },
}, label: {
block: { type: String,
type: Boolean, default: null
default: false },
}, loading: {
label: { type: Boolean,
type: String, default: false
default: null },
}, disabled: {
loading: { type: Boolean,
type: Boolean, default: false
default: false },
}, size: {
disabled: { type: String,
type: Boolean, default: 'md',
default: false validator (value: string) {
}, return Object.keys($ui.button.size).includes(value)
size: {
type: String,
default: 'md',
validator (value) {
return Object.keys($ui.button.size).includes(value)
}
},
variant: {
type: String,
default: 'primary',
validator (value) {
return Object.keys($ui.button.variant).includes(value)
}
},
icon: {
type: String,
default: null
},
loadingIcon: {
type: String,
default: () => $ui.button.icon.loading
},
trailing: {
type: Boolean,
default: false
},
leading: {
type: Boolean,
default: false
},
to: {
type: [String, Object],
default: null
},
target: {
type: String,
default: null
},
ariaLabel: {
type: String,
default: null
},
rounded: {
type: Boolean,
default: false
},
baseClass: {
type: String,
default: () => $ui.button.base
},
iconBaseClass: {
type: String,
default: () => $ui.button.icon.base
},
customClass: {
type: String,
default: null
},
square: {
type: Boolean,
default: false
},
truncate: {
type: Boolean,
default: false
} }
}, },
setup (props, { slots }) { variant: {
const button = ref(null) type: String,
default: 'primary',
const buttonIs = computed(() => { validator (value: string) {
if (props.to) { return Object.keys($ui.button.variant).includes(value)
return 'Link'
}
return 'button'
})
const buttonProps = computed(() => {
switch (buttonIs.value) {
case 'Link': return { to: props.to, target: props.target }
default: return { disabled: props.disabled || props.loading, type: props.type }
}
})
const isLeading = computed(() => {
return (props.icon && props.leading) || (props.icon && !props.trailing) || (props.loading && !props.trailing)
})
const isTrailing = computed(() => {
return (props.icon && props.trailing) || (props.loading && props.trailing)
})
const isSquare = computed(() => props.square || (!slots.default && !props.label))
const buttonClass = computed(() => {
return classNames(
props.baseClass,
$ui.button.size[props.size],
$ui.button[isSquare.value ? 'square' : 'spacing'][props.size],
$ui.button.variant[props.variant],
props.block ? 'w-full flex justify-center items-center' : 'inline-flex items-center',
props.rounded ? 'rounded-full' : 'rounded-md',
props.customClass
)
})
const iconName = computed(() => {
if (props.loading) {
return props.loadingIcon
}
return props.icon
})
const iconClass = computed(() => {
return classNames(
props.iconBaseClass,
$ui.button.icon.size[props.size],
isLeading.value && (!!slots.default || !!props.label?.length) && $ui.button.icon.leading.spacing[props.size],
isTrailing.value && (!!slots.default || !!props.label?.length) && $ui.button.icon.trailing.spacing[props.size],
props.loading && 'animate-spin'
)
})
return {
button,
buttonIs,
buttonProps,
buttonClass,
isLeading,
isTrailing,
iconName,
iconClass
} }
},
icon: {
type: String,
default: null
},
loadingIcon: {
type: String,
default: () => $ui.button.icon.loading
},
trailing: {
type: Boolean,
default: false
},
leading: {
type: Boolean,
default: false
},
to: {
type: [String, Object],
default: null
},
target: {
type: String,
default: null
},
ariaLabel: {
type: String,
default: null
},
rounded: {
type: Boolean,
default: false
},
baseClass: {
type: String,
default: () => $ui.button.base
},
iconBaseClass: {
type: String,
default: () => $ui.button.icon.base
},
customClass: {
type: String,
default: null
},
square: {
type: Boolean,
default: false
},
truncate: {
type: Boolean,
default: false
} }
} })
const slots = useSlots()
const button = ref(null)
const buttonIs = computed(() => {
if (props.to) {
return NuxtLink
}
return 'button'
})
const buttonProps = computed(() => {
if (props.to) {
return { to: props.to, target: props.target }
} else {
return { disabled: props.disabled || props.loading, type: props.type }
}
})
const isLeading = computed(() => {
return (props.icon && props.leading) || (props.icon && !props.trailing) || (props.loading && !props.trailing)
})
const isTrailing = computed(() => {
return (props.icon && props.trailing) || (props.loading && props.trailing)
})
const isSquare = computed(() => props.square || (!slots.default && !props.label))
const buttonClass = computed(() => {
return classNames(
props.baseClass,
$ui.button.size[props.size],
$ui.button[isSquare.value ? 'square' : 'spacing'][props.size],
$ui.button.variant[props.variant],
props.block ? 'w-full flex justify-center items-center' : 'inline-flex items-center',
props.rounded ? 'rounded-full' : 'rounded-md',
props.customClass
)
})
const iconName = computed(() => {
if (props.loading) {
return props.loadingIcon
}
return props.icon
})
const iconClass = computed(() => {
return classNames(
props.iconBaseClass,
$ui.button.icon.size[props.size],
isLeading.value && (!!slots.default || !!props.label?.length) && $ui.button.icon.leading.spacing[props.size],
isTrailing.value && (!!slots.default || !!props.label?.length) && $ui.button.icon.trailing.spacing[props.size],
props.loading && 'animate-spin'
)
})
</script> </script>

View File

@@ -18,8 +18,8 @@
> >
<MenuItems :class="baseClass" static> <MenuItems :class="baseClass" static>
<div v-for="(subItems, index) of items" :key="index" class="py-1"> <div v-for="(subItems, index) of items" :key="index" class="py-1">
<MenuItem v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ active, disabled }" :disabled="item.disabled"> <MenuItem v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ active, disabled }" :disabled="item.disabled" as="div">
<Component v-bind="item" :is="(item.to && 'Link') || (item.click && 'button') || 'div'" :class="resolveItemClass({ active, disabled })" @click="onItemClick(item)" @mouseover="$emit('hover', item)"> <Component v-bind="item" :is="(item.to && NuxtLink) || (item.click && 'button') || 'div'" :class="resolveItemClass({ active, disabled })" @click="onItemClick(item)" @mouseover="$emit('hover', item)">
<slot :name="item.slot" :item="item"> <slot :name="item.slot" :item="item">
<Icon v-if="item.icon" :name="item.icon" :class="itemIconClass" /> <Icon v-if="item.icon" :name="item.icon" :class="itemIconClass" />
<Avatar v-if="item.avatar" :src="item.avatar" :alt="item.label" :class="itemAvatarClass" size="xs" /> <Avatar v-if="item.avatar" :src="item.avatar" :alt="item.label" :class="itemAvatarClass" size="xs" />
@@ -35,7 +35,7 @@
</Menu> </Menu>
</template> </template>
<script> <script setup lang="ts">
import { import {
Menu, Menu,
MenuButton, MenuButton,
@@ -43,195 +43,176 @@ import {
MenuItem MenuItem
} from '@headlessui/vue' } from '@headlessui/vue'
import type { Ref } from 'vue'
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import Icon from '../elements/Icon' import Icon from '../elements/Icon'
import Avatar from '../elements/Avatar' import Avatar from '../elements/Avatar'
import Link from '../elements/Link'
import { classNames, usePopper } from '../../utils' import { classNames, usePopper } from '../../utils'
import $ui from '#build/ui' import $ui from '#build/ui'
import NuxtLink from '#app/components/nuxt-link'
export default { const props = defineProps({
components: { items: {
Menu, type: Array,
MenuButton, default: () => []
MenuItems,
MenuItem,
Icon,
Avatar,
Link
}, },
props: { placement: {
items: { type: String,
type: Array, default: 'bottom-end',
default: () => [] validator: (value: string) => {
}, return ['auto', 'auto-start', 'auto-end', 'top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'right', 'right-start', 'right-end', 'left', 'left-start', 'left-end'].includes(value)
placement: {
type: String,
default: 'bottom-end',
validator: (value) => {
return ['auto', 'auto-start', 'auto-end', 'top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'right', 'right-start', 'right-end', 'left', 'left-start', 'left-end'].includes(value)
}
},
strategy: {
type: String,
default: 'fixed',
validator: (value) => {
return ['absolute', 'fixed'].includes(value)
}
},
mode: {
type: String,
default: 'click',
validator: (value) => {
return ['click', 'hover'].includes(value)
}
},
wrapperClass: {
type: String,
default: () => $ui.dropdown.wrapper
},
containerClass: {
type: String,
default: () => $ui.dropdown.container
},
baseClass: {
type: String,
default: () => $ui.dropdown.base
},
itemBaseClass: {
type: String,
default: () => $ui.dropdown.item.base
},
itemActiveClass: {
type: String,
default: () => $ui.dropdown.item.active
},
itemInactiveClass: {
type: String,
default: () => $ui.dropdown.item.inactive
},
itemDisabledClass: {
type: String,
default: () => $ui.dropdown.item.disabled
},
itemIconClass: {
type: String,
default: () => $ui.dropdown.item.icon
},
itemAvatarClass: {
type: String,
default: () => $ui.dropdown.item.avatar
} }
}, },
emits: ['hover'], strategy: {
setup (props) { type: String,
const [trigger, container] = usePopper({ default: 'fixed',
placement: props.placement, validator: (value: string) => {
strategy: props.strategy, return ['absolute', 'fixed'].includes(value)
modifiers: [{
name: 'offset',
options: {
offset: 0
}
},
{
name: 'computeStyles',
options: {
gpuAcceleration: false,
adaptive: false
}
},
{
name: 'preventOverflow',
options: {
padding: 8
}
}]
})
function resolveItemClass ({ active, disabled }) {
return classNames(
props.itemBaseClass,
active ? props.itemActiveClass : props.itemInactiveClass,
disabled && props.itemDisabledClass
)
} }
},
function onItemClick (item) { mode: {
if (item.disabled) { type: String,
return default: 'click',
} validator: (value: string) => {
return ['click', 'hover'].includes(value)
if (item.click) {
item.click()
}
} }
},
wrapperClass: {
type: String,
default: () => $ui.dropdown.wrapper
},
containerClass: {
type: String,
default: () => $ui.dropdown.container
},
baseClass: {
type: String,
default: () => $ui.dropdown.base
},
itemBaseClass: {
type: String,
default: () => $ui.dropdown.item.base
},
itemActiveClass: {
type: String,
default: () => $ui.dropdown.item.active
},
itemInactiveClass: {
type: String,
default: () => $ui.dropdown.item.inactive
},
itemDisabledClass: {
type: String,
default: () => $ui.dropdown.item.disabled
},
itemIconClass: {
type: String,
default: () => $ui.dropdown.item.icon
},
itemAvatarClass: {
type: String,
default: () => $ui.dropdown.item.avatar
}
})
const menuApi = ref(null) defineEmits(['hover'])
let openTimeout = null
let closeTimeout = null
onMounted(() => {
setTimeout(() => {
const menuProvides = trigger.value?.$.provides
const menuProvidesSymbols = Object.getOwnPropertySymbols(menuProvides)
menuApi.value = menuProvidesSymbols.length && menuProvides[menuProvidesSymbols[0]]
// stop trigger click propagation on hover
menuApi.value.buttonRef.addEventListener('click', (e) => {
if (props.mode === 'hover') {
e.stopPropagation()
}
}, true)
}, 0)
})
function onMouseOver () { const [trigger, container] = usePopper({
if (props.mode !== 'hover' || !menuApi.value) { placement: props.placement,
return strategy: props.strategy,
} modifiers: [{
name: 'offset',
// cancel programmed closing options: {
if (closeTimeout) { offset: 0
clearTimeout(closeTimeout)
closeTimeout = null
}
// dropdown already open
if (menuApi.value.menuState === 0) {
return
}
openTimeout = openTimeout || setTimeout(() => {
menuApi.value.openMenu && menuApi.value.openMenu()
openTimeout = null
}, 50)
} }
},
function onMouseLeave () { {
if (props.mode !== 'hover' || !menuApi.value) { name: 'computeStyles',
return options: {
} gpuAcceleration: false,
adaptive: false
// cancel programmed opening
if (openTimeout) {
clearTimeout(openTimeout)
openTimeout = null
}
// dropdown already closed
if (menuApi.value.menuState === 1) {
return
}
closeTimeout = closeTimeout || setTimeout(() => {
menuApi.value.closeMenu && menuApi.value.closeMenu()
closeTimeout = null
}, 0)
} }
},
return { {
trigger, name: 'preventOverflow',
container, options: {
onItemClick, padding: 8
onMouseOver,
onMouseLeave,
resolveItemClass
} }
}]
})
function resolveItemClass ({ active, disabled }: { active: boolean, disabled: boolean }) {
return classNames(
props.itemBaseClass,
active ? props.itemActiveClass : props.itemInactiveClass,
disabled && props.itemDisabledClass
)
}
function onItemClick (item: any) {
if (item.disabled) {
return
}
if (item.click) {
item.click()
} }
} }
const menuApi: Ref<any> = ref(null)
let openTimeout: NodeJS.Timeout | null = null
let closeTimeout: NodeJS.Timeout | null = null
onMounted(() => {
setTimeout(() => {
const menuProvides = trigger.value?.$.provides
const menuProvidesSymbols = Object.getOwnPropertySymbols(menuProvides)
menuApi.value = menuProvidesSymbols.length && menuProvides[menuProvidesSymbols[0]]
// stop trigger click propagation on hover
menuApi.value?.buttonRef.addEventListener('click', (e: Event) => {
if (props.mode === 'hover') {
e.stopPropagation()
}
}, true)
}, 0)
})
function onMouseOver () {
if (props.mode !== 'hover' || !menuApi.value) {
return
}
// cancel programmed closing
if (closeTimeout) {
clearTimeout(closeTimeout)
closeTimeout = null
}
// dropdown already open
if (menuApi.value.menuState === 0) {
return
}
openTimeout = openTimeout || setTimeout(() => {
menuApi.value.openMenu && menuApi.value.openMenu()
openTimeout = null
}, 50)
}
function onMouseLeave () {
if (props.mode !== 'hover' || !menuApi.value) {
return
}
// cancel programmed opening
if (openTimeout) {
clearTimeout(openTimeout)
openTimeout = null
}
// dropdown already closed
if (menuApi.value.menuState === 1) {
return
}
closeTimeout = closeTimeout || setTimeout(() => {
menuApi.value.closeMenu && menuApi.value.closeMenu()
closeTimeout = null
}, 0)
}
</script> </script>

View File

@@ -22,53 +22,49 @@
</router-link> </router-link>
</template> </template>
<script> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { RouterLink } from 'vue-router' import { RouterLink } from 'vue-router'
export default { const props = defineProps({
name: 'Link', ...RouterLink.props,
inheritAttrs: false, to: {
props: { type: [String, Object],
...RouterLink.props, default: null
to: {
type: [String, Object],
default: null
},
inactiveClass: {
type: String,
default: ''
},
exact: {
type: Boolean,
default: false
},
target: {
type: String,
default: null
}
}, },
setup (props) { inactiveClass: {
const isExternalLink = computed(() => { type: String,
return typeof props.to === 'string' && props.to.startsWith('http') default: ''
}) },
const isButton = computed(() => { exact: {
return !props.to type: Boolean,
}) default: false
},
target: {
type: String,
default: null
}
})
function resolveLinkClass ({ isActive, isExactActive }) { const isExternalLink = computed(() => {
if (props.exact) { return typeof props.to === 'string' && props.to.startsWith('http')
return isExactActive ? props.activeClass : props.inactiveClass })
} else { const isButton = computed(() => {
return isActive ? props.activeClass : props.inactiveClass return !props.to
} })
}
return { function resolveLinkClass ({ isActive, isExactActive }: { isActive: boolean, isExactActive: boolean }) {
isButton, if (props.exact) {
isExternalLink, return isExactActive ? props.activeClass : props.inactiveClass
resolveLinkClass } else {
} return isActive ? props.activeClass : props.inactiveClass
} }
} }
</script> </script>
<script lang="ts">
export default {
name: 'Link',
inheritAttrs: false
}
</script>

View File

@@ -9,95 +9,92 @@
{{ title }} {{ title }}
</p> </p>
<p v-if="link" class="mt-3 text-sm leading-5 md:mt-0 md:ml-6"> <p v-if="link" class="mt-3 text-sm leading-5 md:mt-0 md:ml-6">
<Link <NuxtLink
:to="to" :to="to"
class="whitespace-nowrap font-medium" class="whitespace-nowrap font-medium"
:class="linkClass" :class="linkClass"
@click="click && click()" @click="click && click()"
> >
{{ link }} &rarr; {{ link }} &rarr;
</Link> </NuxtLink>
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { computed } from 'vue'
import Icon from '../elements/Icon' import Icon from '../elements/Icon'
import Link from '../elements/Link'
export default { const props = defineProps({
components: { variant: {
Icon, type: String,
Link default: 'info',
}, validator (value: string) {
props: { return ['info', 'warning', 'error', 'success'].includes(value)
variant: {
type: String,
default: 'info',
validator (value) {
return ['info', 'warning', 'error', 'success'].includes(value)
}
},
to: {
type: [String, Object],
default: null
},
click: {
type: Function,
default: null
},
title: {
type: String,
default: null
},
link: {
type: String,
default: null
} }
}, },
computed: { to: {
iconName () { type: [String, Object],
return ({ default: null
info: 'heroicons-solid:information-circle', },
warning: 'heroicons-solid:exclamation', click: {
error: 'heroicons-solid:x-circle', type: Function,
success: 'heroicons-solid:check-circle' default: null
})[this.variant] },
}, title: {
variantClass () { type: String,
return ({ default: null
info: 'bg-blue-50', },
warning: 'bg-orange-50', link: {
error: 'bg-red-50', type: String,
success: 'bg-green-50' default: null
})[this.variant]
},
iconClass () {
return ({
info: 'text-blue-400',
warning: 'text-orange-400',
error: 'text-red-400',
success: 'text-green-400'
})[this.variant]
},
titleClass () {
return ({
info: 'text-blue-700',
warning: 'text-orange-700',
error: 'text-red-700',
success: 'text-green-700'
})[this.variant]
},
linkClass () {
return ({
info: 'text-blue-700 hover:text-blue-600',
warning: 'text-orange-700 hover:text-orange-600',
error: 'text-red-700 hover:text-red-600',
success: 'text-green-700 hover:text-green-600'
})[this.variant]
}
} }
} })
const iconName = computed(() => {
return ({
info: 'heroicons-solid:information-circle',
warning: 'heroicons-solid:exclamation',
error: 'heroicons-solid:x-circle',
success: 'heroicons-solid:check-circle'
})[props.variant]
})
const variantClass = computed(() => {
return ({
info: 'bg-blue-50',
warning: 'bg-orange-50',
error: 'bg-red-50',
success: 'bg-green-50'
})[props.variant]
})
const iconClass = computed(() => {
return ({
info: 'text-blue-400',
warning: 'text-orange-400',
error: 'text-red-400',
success: 'text-green-400'
})[props.variant]
})
const titleClass = computed(() => {
return ({
info: 'text-blue-700',
warning: 'text-orange-700',
error: 'text-red-700',
success: 'text-green-700'
})[props.variant]
})
const linkClass = computed(() => {
return ({
info: 'text-blue-700 hover:text-blue-600',
warning: 'text-orange-700 hover:text-orange-600',
error: 'text-red-700 hover:text-red-600',
success: 'text-green-700 hover:text-green-600'
})[props.variant]
})
</script> </script>

View File

@@ -26,88 +26,81 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { classNames } from '../../utils' import { classNames } from '../../utils'
import $ui from '#build/ui' import $ui from '#build/ui'
export default { const props = defineProps({
props: { value: {
value: { type: [String, Number, Boolean],
type: [String, Number, Boolean], default: null
default: null
},
modelValue: {
type: [String, Number, Boolean, Array],
default: null
},
name: {
type: String,
default: null
},
disabled: {
type: Boolean,
default: false
},
help: {
type: String,
default: null
},
label: {
type: String,
default: null
},
required: {
type: Boolean,
default: false
},
wrapperClass: {
type: String,
default: () => $ui.checkbox.wrapper
},
baseClass: {
type: String,
default: () => $ui.checkbox.base
},
labelClass: {
type: String,
default: () => $ui.checkbox.label
},
requiredClass: {
type: String,
default: () => $ui.checkbox.required
},
helpClass: {
type: String,
default: () => $ui.checkbox.help
},
customClass: {
type: String,
default: null
}
}, },
emits: ['update:modelValue', 'focus', 'blur'], modelValue: {
setup (props, { emit }) { type: [String, Number, Boolean, Array],
const isChecked = computed({ default: null
get () { },
return props.modelValue name: {
}, type: String,
set (value) { default: null
emit('update:modelValue', value) },
} disabled: {
}) type: Boolean,
default: false
const inputClass = computed(() => { },
return classNames( help: {
props.baseClass, type: String,
props.customClass default: null
) },
}) label: {
type: String,
return { default: null
isChecked, },
inputClass required: {
} type: Boolean,
default: false
},
wrapperClass: {
type: String,
default: () => $ui.checkbox.wrapper
},
baseClass: {
type: String,
default: () => $ui.checkbox.base
},
labelClass: {
type: String,
default: () => $ui.checkbox.label
},
requiredClass: {
type: String,
default: () => $ui.checkbox.required
},
helpClass: {
type: String,
default: () => $ui.checkbox.help
},
customClass: {
type: String,
default: null
} }
} })
const emit = defineEmits(['update:modelValue', 'focus', 'blur'])
const isChecked = computed({
get () {
return props.modelValue
},
set (value) {
emit('update:modelValue', value)
}
})
const inputClass = computed(() => {
return classNames(
props.baseClass,
props.customClass
)
})
</script> </script>

View File

@@ -21,67 +21,65 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import $ui from '#build/ui' import $ui from '#build/ui'
export default { defineProps({
props: { name: {
name: { type: String,
type: String, default: null
default: null },
}, label: {
label: { type: String,
type: String, default: null
default: null },
}, description: {
description: { type: String,
type: String, default: null
default: null },
}, required: {
required: { type: Boolean,
type: Boolean, default: false
default: false },
}, help: {
help: { type: String,
type: String, default: null
default: null },
}, hint: {
hint: { type: String,
type: String, default: null
default: null },
}, wrapperClass: {
wrapperClass: { type: String,
type: String, default: () => $ui.formGroup.wrapper
default: () => $ui.formGroup.wrapper },
}, containerClass: {
containerClass: { type: String,
type: String, default: () => $ui.formGroup.container
default: () => $ui.formGroup.container },
}, labelClass: {
labelClass: { type: String,
type: String, default: () => $ui.formGroup.label
default: () => $ui.formGroup.label },
}, labelWrapperClass: {
labelWrapperClass: { type: String,
type: String, default: () => $ui.formGroup.labelWrapper
default: () => $ui.formGroup.labelWrapper },
}, descriptionClass: {
descriptionClass: { type: String,
type: String, default: () => $ui.formGroup.description
default: () => $ui.formGroup.description },
}, requiredClass: {
requiredClass: { type: String,
type: String, default: () => $ui.formGroup.required
default: () => $ui.formGroup.required },
}, hintClass: {
hintClass: { type: String,
type: String, default: () => $ui.formGroup.hint
default: () => $ui.formGroup.hint },
}, helpClass: {
helpClass: { type: String,
type: String, default: () => $ui.formGroup.help
default: () => $ui.formGroup.help
}
} }
} })
</script> </script>

View File

@@ -27,180 +27,164 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import type { Ref } from 'vue'
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 { classNames } from '../../utils'
import $ui from '#build/ui' import $ui from '#build/ui'
export default { const props = defineProps({
components: { modelValue: {
Icon type: [String, Number],
default: ''
}, },
props: { type: {
modelValue: { type: String,
type: [String, Number], default: 'text'
default: '' },
}, name: {
type: { type: String,
type: String, required: true
default: 'text' },
}, placeholder: {
name: { type: String,
type: String, default: null
required: true },
}, required: {
placeholder: { type: Boolean,
type: String, default: false
default: null },
}, disabled: {
required: { type: Boolean,
type: Boolean, default: false
default: false },
}, readonly: {
disabled: { type: Boolean,
type: Boolean, default: false
default: false },
}, autofocus: {
readonly: { type: Boolean,
type: Boolean, default: false
default: false },
}, autocomplete: {
autofocus: { type: String,
type: Boolean, default: null
default: false },
}, spellcheck: {
autocomplete: { type: String,
type: String, default: null
default: null },
}, icon: {
spellcheck: { type: String,
type: String, default: null
default: null },
}, loadingIcon: {
icon: { type: String,
type: String, default: () => $ui.input.icon.loading
default: null },
}, trailing: {
loadingIcon: { type: Boolean,
type: String, default: false
default: () => $ui.input.icon.loading },
}, leading: {
trailing: { type: Boolean,
type: Boolean, default: false
default: false },
}, size: {
leading: { type: String,
type: Boolean, default: 'md',
default: false validator (value: string) {
}, return Object.keys($ui.input.size).includes(value)
size: {
type: String,
default: 'md',
validator (value) {
return Object.keys($ui.input.size).includes(value)
}
},
wrapperClass: {
type: String,
default: () => $ui.input.wrapper
},
baseClass: {
type: String,
default: () => $ui.input.base
},
iconBaseClass: {
type: String,
default: () => $ui.input.icon.base
},
customClass: {
type: String,
default: null
},
appearance: {
type: String,
default: 'default',
validator (value) {
return Object.keys($ui.input.appearance).includes(value)
}
},
loading: {
type: Boolean,
default: false
} }
}, },
emits: ['update:modelValue', 'focus', 'blur'], wrapperClass: {
setup (props, { emit }) { type: String,
const input = ref(null) default: () => $ui.input.wrapper
},
const autoFocus = () => { baseClass: {
if (props.autofocus) { type: String,
input.value.focus() default: () => $ui.input.base
} },
iconBaseClass: {
type: String,
default: () => $ui.input.icon.base
},
customClass: {
type: String,
default: null
},
appearance: {
type: String,
default: 'default',
validator (value: string) {
return Object.keys($ui.input.appearance).includes(value)
} }
},
loading: {
type: Boolean,
default: false
}
})
const onInput = (value) => { const emit = defineEmits(['update:modelValue', 'focus', 'blur'])
emit('update:modelValue', value)
}
onMounted(() => { const input: Ref<HTMLInputElement> = ref(null)
setTimeout(() => {
autoFocus()
}, 100)
})
const isLeading = computed(() => { const autoFocus = () => {
return (props.icon && props.leading) || (props.icon && !props.trailing) || (props.loading && !props.trailing) if (props.autofocus) {
}) input.value.focus()
const isTrailing = computed(() => {
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(() => {
if (props.loading) {
return props.loadingIcon
}
return props.icon
})
const iconClass = computed(() => {
return classNames(
props.iconBaseClass,
$ui.input.icon.size[props.size],
isLeading.value && $ui.input.icon.leading.spacing[props.size],
isTrailing.value && $ui.input.icon.trailing.spacing[props.size],
props.loading && 'animate-spin'
)
})
const iconLeadingWrapperClass = $ui.input.icon.leading.wrapper
const iconTrailingWrapperClass = $ui.input.icon.trailing.wrapper
return {
input,
onInput,
inputClass,
iconName,
iconClass,
iconLeadingWrapperClass,
iconTrailingWrapperClass,
isLeading,
isTrailing
}
} }
} }
const onInput = (value: string) => {
emit('update:modelValue', value)
}
onMounted(() => {
setTimeout(() => {
autoFocus()
}, 100)
})
const isLeading = computed(() => {
return (props.icon && props.leading) || (props.icon && !props.trailing) || (props.loading && !props.trailing)
})
const isTrailing = computed(() => {
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(() => {
if (props.loading) {
return props.loadingIcon
}
return props.icon
})
const iconClass = computed(() => {
return classNames(
props.iconBaseClass,
$ui.input.icon.size[props.size],
isLeading.value && $ui.input.icon.leading.spacing[props.size],
isTrailing.value && $ui.input.icon.trailing.spacing[props.size],
props.loading && 'animate-spin'
)
})
const iconLeadingWrapperClass = $ui.input.icon.leading.wrapper
const iconTrailingWrapperClass = $ui.input.icon.trailing.wrapper
</script> </script>

View File

@@ -26,88 +26,81 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { classNames } from '../../utils' import { classNames } from '../../utils'
import $ui from '#build/ui' import $ui from '#build/ui'
export default { const props = defineProps({
props: { value: {
value: { type: [String, Number, Boolean],
type: [String, Number, Boolean], default: null
default: null
},
modelValue: {
type: [String, Number, Boolean, Object],
default: null
},
name: {
type: String,
default: null
},
disabled: {
type: Boolean,
default: false
},
help: {
type: String,
default: null
},
label: {
type: String,
default: null
},
required: {
type: Boolean,
default: false
},
wrapperClass: {
type: String,
default: () => $ui.radio.wrapper
},
baseClass: {
type: String,
default: () => $ui.radio.base
},
labelClass: {
type: String,
default: () => $ui.radio.label
},
requiredClass: {
type: String,
default: () => $ui.radio.required
},
helpClass: {
type: String,
default: () => $ui.radio.help
},
customClass: {
type: String,
default: null
}
}, },
emits: ['update:modelValue', 'focus', 'blur'], modelValue: {
setup (props, { emit }) { type: [String, Number, Boolean, Object],
const isChecked = computed({ default: null
get () { },
return props.modelValue name: {
}, type: String,
set (value) { default: null
emit('update:modelValue', value) },
} disabled: {
}) type: Boolean,
default: false
const radioClass = computed(() => { },
return classNames( help: {
props.baseClass, type: String,
props.customClass default: null
) },
}) label: {
type: String,
return { default: null
isChecked, },
radioClass required: {
} type: Boolean,
default: false
},
wrapperClass: {
type: String,
default: () => $ui.radio.wrapper
},
baseClass: {
type: String,
default: () => $ui.radio.base
},
labelClass: {
type: String,
default: () => $ui.radio.label
},
requiredClass: {
type: String,
default: () => $ui.radio.required
},
helpClass: {
type: String,
default: () => $ui.radio.help
},
customClass: {
type: String,
default: null
} }
} })
const emit = defineEmits(['update:modelValue', 'focus', 'blur'])
const isChecked = computed({
get () {
return props.modelValue
},
set (value) {
emit('update:modelValue', value)
}
})
const radioClass = computed(() => {
return classNames(
props.baseClass,
props.customClass
)
})
</script> </script>

View File

@@ -42,172 +42,151 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { ref, computed } from 'vue' import { computed } from 'vue'
import { get } from 'lodash-es' import { get } from 'lodash-es'
import Icon from '../elements/Icon' import Icon from '../elements/Icon'
import { classNames } from '../../utils' import { classNames } from '../../utils'
import $ui from '#build/ui' import $ui from '#build/ui'
export default { const props = defineProps({
components: { modelValue: {
Icon type: [String, Number, Object],
default: ''
}, },
props: { name: {
modelValue: { type: String,
type: [String, Number, Object], required: true
default: '' },
}, placeholder: {
name: { type: String,
type: String, default: null
required: true },
}, required: {
placeholder: { type: Boolean,
type: String, default: false
default: null },
}, disabled: {
required: { type: Boolean,
type: Boolean, default: false
default: false },
}, options: {
disabled: { type: Array,
type: Boolean, default: () => []
default: false },
}, size: {
options: { type: String,
type: Array, default: 'md',
default: () => [] validator (value: string) {
}, return Object.keys($ui.select.size).includes(value)
size: {
type: String,
default: 'md',
validator (value) {
return Object.keys($ui.select.size).includes(value)
}
},
wrapperClass: {
type: String,
default: () => $ui.select.wrapper
},
baseClass: {
type: String,
default: () => $ui.select.base
},
iconBaseClass: {
type: String,
default: () => $ui.select.icon.base
},
customClass: {
type: String,
default: null
},
textAttribute: {
type: String,
default: 'text'
},
valueAttribute: {
type: String,
default: 'value'
},
icon: {
type: String,
default: null
} }
}, },
emits: ['update:modelValue'], wrapperClass: {
setup (props, { emit }) { type: String,
const select = ref(null) default: () => $ui.select.wrapper
},
baseClass: {
type: String,
default: () => $ui.select.base
},
iconBaseClass: {
type: String,
default: () => $ui.select.icon.base
},
customClass: {
type: String,
default: null
},
textAttribute: {
type: String,
default: 'text'
},
valueAttribute: {
type: String,
default: 'value'
},
icon: {
type: String,
default: null
}
})
const onInput = (value) => { const emit = defineEmits(['update:modelValue'])
emit('update:modelValue', value)
}
const guessOptionValue = (option) => { const onInput = (value: string) => {
return get(option, props.valueAttribute, get(option, props.textAttribute)) emit('update:modelValue', value)
} }
const guessOptionText = (option) => { const guessOptionValue = (option: any) => {
return get(option, props.textAttribute, get(option, props.valueAttribute)) return get(option, props.valueAttribute, get(option, props.textAttribute))
} }
const normalizeOption = (option) => { const guessOptionText = (option: any) => {
if (['string', 'number', 'boolean'].includes(typeof option)) { return get(option, props.textAttribute, get(option, props.valueAttribute))
return { }
[props.valueAttribute]: option,
[props.textAttribute]: option
}
}
return {
...option,
[props.valueAttribute]: guessOptionValue(option),
[props.textAttribute]: guessOptionText(option)
}
}
const normalizedOptions = computed(() => {
return props.options.map(option => normalizeOption(option))
})
const normalizedOptionsWithPlaceholder = computed(() => {
if (!props.placeholder) {
return normalizedOptions.value
}
return [
{
[props.valueAttribute]: '',
[props.textAttribute]: props.placeholder,
disabled: true
},
...normalizedOptions.value
]
})
const normalizedValue = computed(() => {
const foundOption = normalizedOptionsWithPlaceholder.value.find(option => option.value === props.modelValue)
if (!foundOption) {
return ''
}
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.wrapper
const normalizeOption = (option: any) => {
if (['string', 'number', 'boolean'].includes(typeof option)) {
return { return {
select, [props.valueAttribute]: option,
onInput, [props.textAttribute]: option
guessOptionValue,
guessOptionText,
normalizeOption,
normalizedOptions,
normalizedOptionsWithPlaceholder,
normalizedValue,
selectClass,
iconClass,
iconWrapperClass
} }
} }
return {
...option,
[props.valueAttribute]: guessOptionValue(option),
[props.textAttribute]: guessOptionText(option)
}
} }
const normalizedOptions = computed(() => {
return props.options.map(option => normalizeOption(option))
})
const normalizedOptionsWithPlaceholder = computed(() => {
if (!props.placeholder) {
return normalizedOptions.value
}
return [
{
[props.valueAttribute]: '',
[props.textAttribute]: props.placeholder,
disabled: true
},
...normalizedOptions.value
]
})
const normalizedValue = computed(() => {
const foundOption = normalizedOptionsWithPlaceholder.value.find(option => option.value === props.modelValue)
if (!foundOption) {
return ''
}
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.wrapper
</script> </script>

View File

@@ -18,145 +18,138 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import type { Ref } from 'vue'
import { ref, computed, watch, onMounted, nextTick } from 'vue' import { ref, computed, watch, onMounted, nextTick } from 'vue'
import { classNames } from '../../utils' import { classNames } from '../../utils'
import $ui from '#build/ui' import $ui from '#build/ui'
export default { const props = defineProps({
props: { modelValue: {
modelValue: { type: [String, Number],
type: [String, Number], default: ''
default: '' },
}, name: {
name: { type: String,
type: String, required: true
required: true },
}, placeholder: {
placeholder: { type: String,
type: String, default: null
default: null },
}, required: {
required: { type: Boolean,
type: Boolean, default: false
default: false },
}, disabled: {
disabled: { type: Boolean,
type: Boolean, default: false
default: false },
}, rows: {
rows: { type: Number,
type: Number, default: 3
default: 3 },
}, autoresize: {
autoresize: { type: Boolean,
type: Boolean, default: false
default: false },
}, autofocus: {
autofocus: { type: Boolean,
type: Boolean, default: false
default: false },
}, autocomplete: {
autocomplete: { type: String,
type: String, default: null
default: null },
}, appearance: {
appearance: { type: String,
type: String, default: 'default',
default: 'default', validator (value: string) {
validator (value) { return Object.keys($ui.textarea.appearance).includes(value)
return Object.keys($ui.textarea.appearance).includes(value)
}
},
resize: {
type: Boolean,
default: true
},
size: {
type: String,
default: 'md',
validator (value) {
return Object.keys($ui.textarea.size).includes(value)
}
},
wrapperClass: {
type: String,
default: () => $ui.textarea.wrapper
},
baseClass: {
type: String,
default: () => $ui.textarea.base
},
customClass: {
type: String,
default: null
} }
}, },
emits: ['update:modelValue', 'focus', 'blur'], resize: {
setup (props, { emit }) { type: Boolean,
const textarea = ref(null) default: true
},
size: {
type: String,
default: 'md',
validator (value: string) {
return Object.keys($ui.textarea.size).includes(value)
}
},
wrapperClass: {
type: String,
default: () => $ui.textarea.wrapper
},
baseClass: {
type: String,
default: () => $ui.textarea.base
},
customClass: {
type: String,
default: null
}
})
const autoFocus = () => { const emit = defineEmits(['update:modelValue', 'focus', 'blur'])
if (props.autofocus) {
textarea.value.focus() const textarea: Ref<HTMLTextAreaElement> = ref(null)
}
const autoFocus = () => {
if (props.autofocus) {
textarea.value.focus()
}
}
const autoResize = () => {
if (props.autoresize) {
if (!textarea.value) {
return
} }
const autoResize = () => { textarea.value.rows = props.rows
if (props.autoresize) {
if (!textarea.value) {
return
}
textarea.value.rows = props.rows const styles = window.getComputedStyle(textarea.value)
const paddingTop = parseInt(styles.paddingTop)
const paddingBottom = parseInt(styles.paddingBottom)
const padding = paddingTop + paddingBottom
const lineHeight = parseInt(styles.lineHeight)
const { scrollHeight } = textarea.value
const newRows = (scrollHeight - padding) / lineHeight
const styles = window.getComputedStyle(textarea.value) if (newRows > props.rows) {
const paddingTop = parseInt(styles.paddingTop) textarea.value.rows = newRows
const paddingBottom = parseInt(styles.paddingBottom)
const padding = paddingTop + paddingBottom
const lineHeight = parseInt(styles.lineHeight)
const { scrollHeight } = textarea.value
const newRows = (scrollHeight - padding) / lineHeight
if (newRows > props.rows) {
textarea.value.rows = newRows
}
}
}
const onInput = (value) => {
autoResize()
emit('update:modelValue', value)
}
watch(() => props.modelValue, () => {
nextTick(autoResize)
})
onMounted(() => {
setTimeout(() => {
autoFocus()
autoResize()
}, 100)
})
const textareaClass = computed(() => {
return classNames(
props.baseClass,
$ui.textarea.size[props.size],
$ui.textarea.spacing[props.size],
$ui.textarea.appearance[props.appearance],
!props.resize && 'resize-none',
props.customClass
)
})
return {
textarea,
onInput,
textareaClass
} }
} }
} }
const onInput = (value: string) => {
autoResize()
emit('update:modelValue', value)
}
watch(() => props.modelValue, () => {
nextTick(autoResize)
})
onMounted(() => {
setTimeout(() => {
autoFocus()
autoResize()
}, 100)
})
const textareaClass = computed(() => {
return classNames(
props.baseClass,
$ui.textarea.size[props.size],
$ui.textarea.spacing[props.size],
$ui.textarea.appearance[props.appearance],
!props.resize && 'resize-none',
props.customClass
)
})
</script> </script>

View File

@@ -22,93 +22,86 @@
</component> </component>
</template> </template>
<script> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { classNames } from '../../utils/' import { classNames } from '../../utils/'
import $ui from '#build/ui' import $ui from '#build/ui'
export default { const props = defineProps({
props: { padded: {
padded: { type: Boolean,
type: Boolean, default: false
default: false },
}, rounded: {
rounded: { type: Boolean,
type: Boolean, default: true
default: true },
}, baseClass: {
baseClass: { type: String,
type: String, default: () => $ui.card.base
default: () => $ui.card.base },
}, backgroundClass: {
backgroundClass: { type: String,
type: String, default: () => $ui.card.background
default: () => $ui.card.background },
}, borderColorClass: {
borderColorClass: { type: String,
type: String, default: () => $ui.card.border
default: () => $ui.card.border },
}, shadowClass: {
shadowClass: { type: String,
type: String, default: () => $ui.card.shadow
default: () => $ui.card.shadow },
}, ringClass: {
ringClass: { type: String,
type: String, default: () => $ui.card.ring
default: () => $ui.card.ring },
}, roundedClass: {
roundedClass: { type: String,
type: String, default: () => $ui.card.rounded,
default: () => $ui.card.rounded, validator (value: string) {
validator (value) { return !value || ['sm', 'md', 'lg', 'xl', '2xl', '3xl'].map(size => `rounded-${size}`).includes(value)
return !value || ['sm', 'md', 'lg', 'xl', '2xl', '3xl'].map(size => `rounded-${size}`).includes(value)
}
},
bodyClass: {
type: String,
default: () => $ui.card.body
},
bodyBackgroundClass: {
type: String,
default: null
},
headerClass: {
type: String,
default: () => $ui.card.header
},
headerBackgroundClass: {
type: String,
default: null
},
footerClass: {
type: String,
default: () => $ui.card.footer
},
footerBackgroundClass: {
type: String,
default: null
},
customClass: {
type: String,
default: null
} }
}, },
setup (props) { bodyClass: {
const cardClass = computed(() => { type: String,
return classNames( default: () => $ui.card.body
props.baseClass, },
props.padded && props.rounded && props.roundedClass, bodyBackgroundClass: {
!props.padded && props.rounded && `sm:${props.roundedClass}`, type: String,
props.ringClass, default: null
props.shadowClass, },
props.backgroundClass, headerClass: {
props.customClass type: String,
) default: () => $ui.card.header
}) },
headerBackgroundClass: {
return { type: String,
cardClass default: null
} },
footerClass: {
type: String,
default: () => $ui.card.footer
},
footerBackgroundClass: {
type: String,
default: null
},
customClass: {
type: String,
default: null
} }
} })
const cardClass = computed(() => {
return classNames(
props.baseClass,
props.padded && props.rounded && props.roundedClass,
!props.padded && props.rounded && `sm:${props.roundedClass}`,
props.ringClass,
props.shadowClass,
props.backgroundClass,
props.customClass
)
})
</script> </script>

View File

@@ -30,69 +30,63 @@
</nav> </nav>
</template> </template>
<script> <script setup lang="ts">
import Icon from '../elements/Icon' import Icon from '../elements/Icon'
import Link from '../elements/Link' import Link from '../elements/Link'
import $ui from '#build/ui' import $ui from '#build/ui'
export default { defineProps({
components: { links: {
Icon, type: Array,
Link required: true
}, },
props: { wrapperClass: {
links: { type: String,
type: Array, default: () => $ui.verticalNavigation.wrapper
required: true },
}, baseClass: {
wrapperClass: { type: String,
type: String, default: () => $ui.verticalNavigation.base
default: () => $ui.verticalNavigation.wrapper },
}, spacingClass: {
baseClass: { type: String,
type: String, default: () => $ui.verticalNavigation.spacing
default: () => $ui.verticalNavigation.base },
}, activeClass: {
spacingClass: { type: String,
type: String, default: () => $ui.verticalNavigation.active
default: () => $ui.verticalNavigation.spacing },
}, inactiveClass: {
activeClass: { type: String,
type: String, default: () => $ui.verticalNavigation.inactive
default: () => $ui.verticalNavigation.active },
}, iconBaseClass: {
inactiveClass: { type: String,
type: String, default: () => $ui.verticalNavigation.icon.base
default: () => $ui.verticalNavigation.inactive },
}, iconSpacingClass: {
iconBaseClass: { type: String,
type: String, default: () => $ui.verticalNavigation.icon.spacing
default: () => $ui.verticalNavigation.icon.base },
}, iconActiveClass: {
iconSpacingClass: { type: String,
type: String, default: () => $ui.verticalNavigation.icon.active
default: () => $ui.verticalNavigation.icon.spacing },
}, iconInactiveClass: {
iconActiveClass: { type: String,
type: String, default: () => $ui.verticalNavigation.icon.inactive
default: () => $ui.verticalNavigation.icon.active },
}, badgeBaseClass: {
iconInactiveClass: { type: String,
type: String, default: () => $ui.verticalNavigation.badge.base
default: () => $ui.verticalNavigation.icon.inactive },
}, badgeActiveClass: {
badgeBaseClass: { type: String,
type: String, default: () => $ui.verticalNavigation.badge.active
default: () => $ui.verticalNavigation.badge.base },
}, badgeInactiveClass: {
badgeActiveClass: { type: String,
type: String, default: () => $ui.verticalNavigation.badge.inactive
default: () => $ui.verticalNavigation.badge.active
},
badgeInactiveClass: {
type: String,
default: () => $ui.verticalNavigation.badge.inactive
}
} }
} })
</script> </script>

View File

@@ -53,89 +53,81 @@
</TransitionRoot> </TransitionRoot>
</template> </template>
<script> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { Dialog, DialogPanel, TransitionRoot, TransitionChild } from '@headlessui/vue' import { Dialog, DialogPanel, TransitionRoot, TransitionChild } from '@headlessui/vue'
import { classNames } from '../../utils/' import { classNames } from '../../utils/'
import Card from '../layout/Card' import Card from '../layout/Card'
import $ui from '#build/ui' import $ui from '#build/ui'
export default { const props = defineProps({
components: { modelValue: {
Dialog, type: Boolean,
DialogPanel, default: false
TransitionRoot,
TransitionChild,
Card
}, },
inheritAttrs: false, appear: {
props: { type: Boolean,
modelValue: { default: false
type: Boolean, },
default: false baseClass: {
}, type: String,
appear: { default: () => $ui.modal.base
type: Boolean, },
default: false backgroundClass: {
}, type: String,
baseClass: { default: () => $ui.modal.background
type: String, },
default: () => $ui.modal.base shadowClass: {
}, type: String,
backgroundClass: { default: () => $ui.modal.shadow
type: String, },
default: () => $ui.modal.background ringClass: {
}, type: String,
shadowClass: { default: () => $ui.modal.ring
type: String, },
default: () => $ui.modal.shadow roundedClass: {
}, type: String,
ringClass: { default: () => $ui.modal.rounded
type: String, },
default: () => $ui.modal.ring widthClass: {
}, type: String,
roundedClass: { default: () => $ui.modal.width,
type: String, validator (value: string) {
default: () => $ui.modal.rounded return ['xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl', '6xl', '7xl'].map(width => `max-w-${width}`).includes(value)
},
widthClass: {
type: String,
default: () => $ui.modal.width,
validator (value) {
return ['xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl', '6xl', '7xl'].map(width => `max-w-${width}`).includes(value)
}
} }
}, }
emits: ['update:modelValue', 'close'], })
setup (props, { emit }) {
const isOpen = computed({
get () {
return props.modelValue
},
set (value) {
emit('update:modelValue', value)
}
})
const modalClass = computed(() => { const emit = defineEmits(['update:modelValue', 'close'])
return classNames(
props.baseClass, const isOpen = computed({
get () {
return props.modelValue
},
set (value) {
emit('update:modelValue', value)
}
})
const modalClass = computed(() => {
return classNames(
props.baseClass,
`sm:${props.widthClass}`, `sm:${props.widthClass}`,
props.backgroundClass, props.backgroundClass,
props.shadowClass, props.shadowClass,
props.ringClass, props.ringClass,
props.roundedClass props.roundedClass
) )
}) })
return { function close (value: boolean) {
isOpen, isOpen.value = value
modalClass, emit('close')
close (value) { }
isOpen.value = value </script>
emit('close')
} <script lang="ts">
} export default {
} inheritAttrs: false
} }
</script> </script>

View File

@@ -55,148 +55,131 @@
</transition> </transition>
</template> </template>
<script> <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watchEffect } from 'vue' import { ref, computed, onMounted, onUnmounted, watchEffect } from 'vue'
import Icon from '../elements/Icon' import Icon from '../elements/Icon'
import Button from '../elements/Button' import Button from '../elements/Button'
import { useTimer } from '../../composables/useTimer' import { useTimer } from '../../composables/useTimer'
export default { const props = defineProps({
components: { id: {
Icon, type: String,
Button required: true
}, },
props: { type: {
id: { type: String,
type: String, required: true,
required: true default: 'info',
}, validator (value: string) {
type: { return ['info', 'success', 'error', 'warning'].includes(value)
type: String,
required: true,
default: 'info',
validator (value) {
return ['info', 'success', 'error', 'warning'].includes(value)
}
},
title: {
type: String,
required: true
},
description: {
type: String,
default: null
},
icon: {
type: String,
default: null
},
timeout: {
type: Number,
default: 5000
},
undo: {
type: Function,
default: null
},
callback: {
type: Function,
default: null
} }
}, },
emits: ['close'], title: {
setup (props, { emit }) { type: String,
let timer = null required: true
const remaining = ref(props.timeout) },
description: {
type: String,
default: null
},
icon: {
type: String,
default: null
},
timeout: {
type: Number,
default: 5000
},
undo: {
type: Function,
default: null
},
callback: {
type: Function,
default: null
}
})
const iconName = computed(() => { const emit = defineEmits(['close'])
return props.icon || ({
warning: 'heroicons-outline:exclamation-circle',
info: 'heroicons-outline:information-circle',
success: 'heroicons-outline:check-circle',
error: 'heroicons-outline:x-circle'
})[props.type]
})
const iconClass = computed(() => { let timer: any = null
return ({ const remaining = ref(props.timeout)
warning: 'text-orange-400',
info: 'text-blue-400',
success: 'text-green-400',
error: 'text-red-400'
})[props.type] || 'u-text-gray-400'
})
const progressBarStyle = computed(() => { const iconName = computed(() => {
const remainingPercent = remaining.value / props.timeout * 100 return props.icon || ({
return { width: `${remainingPercent || 0}%` } warning: 'heroicons-outline:exclamation-circle',
}) info: 'heroicons-outline:information-circle',
success: 'heroicons-outline:check-circle',
error: 'heroicons-outline:x-circle'
})[props.type]
})
function onMouseover () { const iconClass = computed(() => {
if (timer) { return ({
timer.pause() warning: 'text-orange-400',
} info: 'text-blue-400',
} success: 'text-green-400',
error: 'text-red-400'
})[props.type] || 'u-text-gray-400'
})
function onMouseleave () { const progressBarStyle = computed(() => {
if (timer) { const remainingPercent = remaining.value / props.timeout * 100
timer.resume() return { width: `${remainingPercent || 0}%` }
} })
}
function onClose () { function onMouseover () {
if (timer) { if (timer) {
timer.stop() timer.pause()
}
if (props.callback) {
props.callback()
}
emit('close')
}
function onUndo () {
if (timer) {
timer.stop()
}
if (props.undo) {
props.undo()
}
emit('close')
}
onMounted(() => {
if (!props.timeout) {
return
}
timer = useTimer(() => {
onClose()
}, props.timeout)
watchEffect(() => {
remaining.value = timer.remaining.value
})
})
onUnmounted(() => {
timer.stop()
})
return {
timer,
iconName,
iconClass,
progressBarStyle,
onMouseover,
onMouseleave,
onClose,
onUndo
}
} }
} }
function onMouseleave () {
if (timer) {
timer.resume()
}
}
function onClose () {
if (timer) {
timer.stop()
}
if (props.callback) {
props.callback()
}
emit('close')
}
function onUndo () {
if (timer) {
timer.stop()
}
if (props.undo) {
props.undo()
}
emit('close')
}
onMounted(() => {
if (!props.timeout) {
return
}
timer = useTimer(() => {
onClose()
}, props.timeout)
watchEffect(() => {
remaining.value = timer.remaining.value
})
})
onUnmounted(() => {
timer.stop()
})
</script> </script>

View File

@@ -24,135 +24,121 @@
</Popover> </Popover>
</template> </template>
<script> <script setup lang="ts">
import type { Ref } from 'vue'
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue' import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
import { usePopper } from '../../utils' import { usePopper } from '../../utils'
export default { const props = defineProps({
components: { placement: {
Popover, type: String,
PopoverButton, default: 'bottom'
PopoverPanel
}, },
props: { strategy: {
placement: { type: String,
type: String, default: 'fixed'
default: 'bottom' },
}, mode: {
strategy: { type: String,
type: String, default: 'click',
default: 'fixed' validator: (value: string) => {
}, return ['click', 'hover'].includes(value)
mode: {
type: String,
default: 'click',
validator: (value) => {
return ['click', 'hover'].includes(value)
}
},
wrapperClass: {
type: String,
default: 'relative'
},
containerClass: {
type: String,
default: 'z-10 py-2'
},
panelClass: {
type: String,
default: ''
} }
}, },
setup (props) { wrapperClass: {
const [trigger, container] = usePopper({ type: String,
placement: props.placement, default: 'relative'
strategy: props.strategy, },
modifiers: [{ containerClass: {
name: 'offset', type: String,
options: { default: 'z-10 py-2'
offset: 0 },
} panelClass: {
}, type: String,
{ default: ''
name: 'computeStyles',
options: {
gpuAcceleration: false,
adaptive: false
}
},
{
name: 'preventOverflow',
options: {
padding: 8
}
}]
})
const popoverApi = ref(null)
let openTimeout = null
let closeTimeout = null
onMounted(() => {
setTimeout(() => {
const popoverProvides = trigger.value?.$.provides
const popoverProvidesSymbols = Object.getOwnPropertySymbols(popoverProvides)
popoverApi.value = popoverProvidesSymbols.length && popoverProvides[popoverProvidesSymbols[0]]
// stop trigger click propagation on hover
popoverApi.value.button.addEventListener('click', (e) => {
if (props.mode === 'hover') {
e.stopPropagation()
}
}, true)
}, 0)
})
function onMouseOver () {
if (props.mode !== 'hover' || !popoverApi.value) {
return
}
// cancel programmed closing
if (closeTimeout) {
clearTimeout(closeTimeout)
closeTimeout = null
}
// dropdown already open
if (popoverApi.value.popoverState === 0) {
return
}
openTimeout = openTimeout || setTimeout(() => {
popoverApi.value.togglePopover && popoverApi.value.togglePopover()
openTimeout = null
}, 50)
}
function onMouseLeave () {
if (props.mode !== 'hover' || !popoverApi.value) {
return
}
// cancel programmed opening
if (openTimeout) {
clearTimeout(openTimeout)
openTimeout = null
}
// dropdown already closed
if (popoverApi.value.popoverState === 1) {
return
}
closeTimeout = closeTimeout || setTimeout(() => {
popoverApi.value.closePopover && popoverApi.value.closePopover()
closeTimeout = null
}, 0)
}
return {
trigger,
container,
onMouseOver,
onMouseLeave
}
} }
})
const [trigger, container] = usePopper({
placement: props.placement,
strategy: props.strategy,
modifiers: [{
name: 'offset',
options: {
offset: 0
}
},
{
name: 'computeStyles',
options: {
gpuAcceleration: false,
adaptive: false
}
},
{
name: 'preventOverflow',
options: {
padding: 8
}
}]
})
const popoverApi: Ref<any> = ref(null)
let openTimeout: NodeJS.Timeout | null = null
let closeTimeout: NodeJS.Timeout | null = null
onMounted(() => {
setTimeout(() => {
const popoverProvides = trigger.value?.$.provides
const popoverProvidesSymbols = Object.getOwnPropertySymbols(popoverProvides)
popoverApi.value = popoverProvidesSymbols.length && popoverProvides[popoverProvidesSymbols[0]]
// stop trigger click propagation on hover
popoverApi.value.button.addEventListener('click', (e: Event) => {
if (props.mode === 'hover') {
e.stopPropagation()
}
}, true)
}, 0)
})
function onMouseOver () {
if (props.mode !== 'hover' || !popoverApi.value) {
return
}
// cancel programmed closing
if (closeTimeout) {
clearTimeout(closeTimeout)
closeTimeout = null
}
// dropdown already open
if (popoverApi.value.popoverState === 0) {
return
}
openTimeout = openTimeout || setTimeout(() => {
popoverApi.value.togglePopover && popoverApi.value.togglePopover()
openTimeout = null
}, 50)
}
function onMouseLeave () {
if (props.mode !== 'hover' || !popoverApi.value) {
return
}
// cancel programmed opening
if (openTimeout) {
clearTimeout(openTimeout)
openTimeout = null
}
// dropdown already closed
if (popoverApi.value.popoverState === 1) {
return
}
closeTimeout = closeTimeout || setTimeout(() => {
popoverApi.value.closePopover && popoverApi.value.closePopover()
closeTimeout = null
}, 0)
} }
</script> </script>

View File

@@ -24,74 +24,65 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { usePopper } from '../../utils' import { usePopper } from '../../utils'
export default { const props = defineProps({
props: { text: {
text: { type: String,
type: String, default: null
default: null },
}, placement: {
placement: { type: String,
type: String, default: 'bottom',
default: 'bottom', validator: (value: string) => {
validator: (value) => { return ['auto', 'auto-start', 'auto-end', 'top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'right', 'right-start', 'right-end', 'left', 'left-start', 'left-end'].includes(value)
return ['auto', 'auto-start', 'auto-end', 'top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'right', 'right-start', 'right-end', 'left', 'left-start', 'left-end'].includes(value)
}
},
strategy: {
type: String,
default: 'fixed',
validator: (value) => {
return ['absolute', 'fixed'].includes(value)
}
},
wrapperClass: {
type: String,
default: 'relative inline-flex'
},
containerClass: {
type: String,
default: 'z-10 py-2'
},
tooltipClass: {
type: String,
default: 'flex items-center justify-center invisible w-auto h-6 max-w-xs px-2 space-x-1 truncate rounded shadow lg:visible u-bg-gray-800 truncate u-text-gray-50 text-xs'
} }
}, },
setup (props) { strategy: {
const open = ref(false) type: String,
const [trigger, container] = usePopper({ default: 'fixed',
placement: props.placement, validator: (value: string) => {
strategy: props.strategy, return ['absolute', 'fixed'].includes(value)
modifiers: [{
name: 'offset',
options: {
offset: 0
}
},
{
name: 'computeStyles',
options: {
gpuAcceleration: false,
adaptive: false
}
},
{
name: 'preventOverflow',
options: {
padding: 8
}
}]
})
return {
open,
trigger,
container
} }
},
wrapperClass: {
type: String,
default: 'relative inline-flex'
},
containerClass: {
type: String,
default: 'z-10 py-2'
},
tooltipClass: {
type: String,
default: 'flex items-center justify-center invisible w-auto h-6 max-w-xs px-2 space-x-1 truncate rounded shadow lg:visible u-bg-gray-800 truncate u-text-gray-50 text-xs'
} }
} })
const open = ref(false)
const [trigger, container] = usePopper({
placement: props.placement,
strategy: props.strategy,
modifiers: [{
name: 'offset',
options: {
offset: 0
}
},
{
name: 'computeStyles',
options: {
gpuAcceleration: false,
adaptive: false
}
},
{
name: 'preventOverflow',
options: {
padding: 8
}
}]
})
</script> </script>