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'
}, {
label: 'Move',
icon: 'heroicons-solid:external-link'
icon: 'heroicons-solid:external-link',
to: 'https://www.google.fr',
target: '_blank'
}],
[{
label: 'Delete',

View File

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

View File

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

View File

@@ -12,174 +12,159 @@
</component>
</template>
<script>
import { ref, computed } from 'vue'
import Link from '../elements/Link'
<script setup lang="ts">
import { ref, computed, useSlots } from 'vue'
import Icon from '../elements/Icon'
import { classNames } from '../../utils'
import NuxtLink from '#app/components/nuxt-link'
import $ui from '#build/ui'
export default {
components: {
Icon,
Link
const props = defineProps({
type: {
type: String,
default: 'button'
},
props: {
type: {
type: String,
default: 'button'
},
block: {
type: Boolean,
default: false
},
label: {
type: String,
default: null
},
loading: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
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
block: {
type: Boolean,
default: false
},
label: {
type: String,
default: null
},
loading: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
size: {
type: String,
default: 'md',
validator (value: string) {
return Object.keys($ui.button.size).includes(value)
}
},
setup (props, { slots }) {
const button = ref(null)
const buttonIs = computed(() => {
if (props.to) {
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
variant: {
type: String,
default: 'primary',
validator (value: string) {
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
}
}
})
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>

View File

@@ -18,8 +18,8 @@
>
<MenuItems :class="baseClass" static>
<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">
<Component v-bind="item" :is="(item.to && 'Link') || (item.click && 'button') || 'div'" :class="resolveItemClass({ active, disabled })" @click="onItemClick(item)" @mouseover="$emit('hover', item)">
<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 && NuxtLink) || (item.click && 'button') || 'div'" :class="resolveItemClass({ active, disabled })" @click="onItemClick(item)" @mouseover="$emit('hover', item)">
<slot :name="item.slot" :item="item">
<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" />
@@ -35,7 +35,7 @@
</Menu>
</template>
<script>
<script setup lang="ts">
import {
Menu,
MenuButton,
@@ -43,195 +43,176 @@ import {
MenuItem
} from '@headlessui/vue'
import type { Ref } from 'vue'
import { ref, onMounted } from 'vue'
import Icon from '../elements/Icon'
import Avatar from '../elements/Avatar'
import Link from '../elements/Link'
import { classNames, usePopper } from '../../utils'
import $ui from '#build/ui'
import NuxtLink from '#app/components/nuxt-link'
export default {
components: {
Menu,
MenuButton,
MenuItems,
MenuItem,
Icon,
Avatar,
Link
const props = defineProps({
items: {
type: Array,
default: () => []
},
props: {
items: {
type: Array,
default: () => []
},
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
placement: {
type: String,
default: 'bottom-end',
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)
}
},
emits: ['hover'],
setup (props) {
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
}
}]
})
function resolveItemClass ({ active, disabled }) {
return classNames(
props.itemBaseClass,
active ? props.itemActiveClass : props.itemInactiveClass,
disabled && props.itemDisabledClass
)
strategy: {
type: String,
default: 'fixed',
validator: (value: string) => {
return ['absolute', 'fixed'].includes(value)
}
function onItemClick (item) {
if (item.disabled) {
return
}
if (item.click) {
item.click()
}
},
mode: {
type: String,
default: 'click',
validator: (value: string) => {
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
}
})
const menuApi = ref(null)
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)
})
defineEmits(['hover'])
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)
const [trigger, container] = usePopper({
placement: props.placement,
strategy: props.strategy,
modifiers: [{
name: 'offset',
options: {
offset: 0
}
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)
},
{
name: 'computeStyles',
options: {
gpuAcceleration: false,
adaptive: false
}
return {
trigger,
container,
onItemClick,
onMouseOver,
onMouseLeave,
resolveItemClass
},
{
name: 'preventOverflow',
options: {
padding: 8
}
}]
})
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>

View File

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

View File

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

View File

@@ -26,88 +26,81 @@
</div>
</template>
<script>
<script setup lang="ts">
import { computed } from 'vue'
import { classNames } from '../../utils'
import $ui from '#build/ui'
export default {
props: {
value: {
type: [String, Number, Boolean],
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
}
const props = defineProps({
value: {
type: [String, Number, Boolean],
default: null
},
emits: ['update:modelValue', 'focus', 'blur'],
setup (props, { emit }) {
const isChecked = computed({
get () {
return props.modelValue
},
set (value) {
emit('update:modelValue', value)
}
})
const inputClass = computed(() => {
return classNames(
props.baseClass,
props.customClass
)
})
return {
isChecked,
inputClass
}
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
}
}
})
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>

View File

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

View File

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

View File

@@ -26,88 +26,81 @@
</div>
</template>
<script>
<script setup lang="ts">
import { computed } from 'vue'
import { classNames } from '../../utils'
import $ui from '#build/ui'
export default {
props: {
value: {
type: [String, Number, Boolean],
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
}
const props = defineProps({
value: {
type: [String, Number, Boolean],
default: null
},
emits: ['update:modelValue', 'focus', 'blur'],
setup (props, { emit }) {
const isChecked = computed({
get () {
return props.modelValue
},
set (value) {
emit('update:modelValue', value)
}
})
const radioClass = computed(() => {
return classNames(
props.baseClass,
props.customClass
)
})
return {
isChecked,
radioClass
}
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
}
}
})
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>

View File

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

View File

@@ -18,145 +18,138 @@
</div>
</template>
<script>
<script setup lang="ts">
import type { Ref } from 'vue'
import { ref, computed, watch, onMounted, nextTick } from 'vue'
import { classNames } from '../../utils'
import $ui from '#build/ui'
export default {
props: {
modelValue: {
type: [String, Number],
default: ''
},
name: {
type: String,
required: true
},
placeholder: {
type: String,
default: null
},
required: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
rows: {
type: Number,
default: 3
},
autoresize: {
type: Boolean,
default: false
},
autofocus: {
type: Boolean,
default: false
},
autocomplete: {
type: String,
default: null
},
appearance: {
type: String,
default: 'default',
validator (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
const props = defineProps({
modelValue: {
type: [String, Number],
default: ''
},
name: {
type: String,
required: true
},
placeholder: {
type: String,
default: null
},
required: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
rows: {
type: Number,
default: 3
},
autoresize: {
type: Boolean,
default: false
},
autofocus: {
type: Boolean,
default: false
},
autocomplete: {
type: String,
default: null
},
appearance: {
type: String,
default: 'default',
validator (value: string) {
return Object.keys($ui.textarea.appearance).includes(value)
}
},
emits: ['update:modelValue', 'focus', 'blur'],
setup (props, { emit }) {
const textarea = ref(null)
resize: {
type: Boolean,
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 = () => {
if (props.autofocus) {
textarea.value.focus()
}
const emit = defineEmits(['update:modelValue', 'focus', 'blur'])
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 = () => {
if (props.autoresize) {
if (!textarea.value) {
return
}
textarea.value.rows = props.rows
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)
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
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
if (newRows > props.rows) {
textarea.value.rows = newRows
}
}
}
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>

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,135 +24,121 @@
</Popover>
</template>
<script>
<script setup lang="ts">
import type { Ref } from 'vue'
import { ref, onMounted } from 'vue'
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
import { usePopper } from '../../utils'
export default {
components: {
Popover,
PopoverButton,
PopoverPanel
const props = defineProps({
placement: {
type: String,
default: 'bottom'
},
props: {
placement: {
type: String,
default: 'bottom'
},
strategy: {
type: String,
default: 'fixed'
},
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: ''
strategy: {
type: String,
default: 'fixed'
},
mode: {
type: String,
default: 'click',
validator: (value: string) => {
return ['click', 'hover'].includes(value)
}
},
setup (props) {
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(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
}
wrapperClass: {
type: String,
default: 'relative'
},
containerClass: {
type: String,
default: 'z-10 py-2'
},
panelClass: {
type: String,
default: ''
}
})
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>

View File

@@ -24,74 +24,65 @@
</div>
</template>
<script>
<script setup lang="ts">
import { ref } from 'vue'
import { usePopper } from '../../utils'
export default {
props: {
text: {
type: String,
default: null
},
placement: {
type: String,
default: 'bottom',
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)
}
},
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 props = defineProps({
text: {
type: String,
default: null
},
placement: {
type: String,
default: 'bottom',
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)
}
},
setup (props) {
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
}
}]
})
return {
open,
trigger,
container
strategy: {
type: String,
default: 'fixed',
validator: (value: string) => {
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'
}
}
})
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>