chore: move to tsup

This commit is contained in:
Benjamin Canac
2021-11-24 16:07:18 +01:00
parent 7a31f1be4f
commit 65a6aa5fda
35 changed files with 132 additions and 16 deletions

View File

@@ -0,0 +1,158 @@
<template>
<span class="relative inline-flex items-center justify-center" :class="avatarClass" @click="goto">
<img v-if="url" :src="url" :alt="alt" :class="[sizeClass, roundedClass]">
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-else-if="gradientPlaceholder" class="w-full h-full overflow-hidden" :class="roundedClass" v-html="gradientPlaceholder" />
<span
v-else-if="placeholder"
class="font-medium leading-none text-white uppercase"
>{{ placeholder }}</span>
<svg
v-else
class="w-full h-full text-tw-gray-300"
:class="roundedClass"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z"
/>
</svg>
<span
v-if="status"
class="absolute top-0 right-0 block rounded-full ring-1 ring-white dark:ring-black"
:class="statusClass"
/>
</span>
</template>
<script>
import avatar from 'gradient-avatar'
export default {
props: {
src: {
type: [String, Boolean],
default: null
},
alt: {
type: String,
default: null
},
to: {
type: String,
default: null
},
size: {
type: String,
default: 'md',
validator (value) {
return ['xxxs', 'xxs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl'].includes(value)
}
},
rounded: {
type: Boolean,
default: false
},
gradient: {
type: Boolean,
default: false
},
status: {
type: String,
default: null,
validator (value) {
return ['online', 'idle', 'invisible', 'donotdisturb', 'focus'].includes(value)
}
}
},
computed: {
url () {
if (typeof this.src === 'boolean') {
return null
}
return this.src
},
placeholder () {
if (!this.alt) {
return
}
return this.alt.split(' ').map(word => word.charAt(0)).join('')
},
gradientPlaceholder () {
if (!this.gradient) {
return
}
return avatar(this.alt || new Date().toString())
},
sizeClass () {
return ({
xxxs: 'h-4 w-4 text-xs',
xxs: 'h-5 w-5 text-xs',
xs: 'h-6 w-6 text-xs',
sm: 'h-8 w-8 text-sm',
md: 'h-10 w-10 text-md',
lg: 'h-12 w-12 text-lg',
xl: 'h-14 w-14 text-xl',
'2xl': 'h-16 w-16 text-2xl',
'3xl': 'h-20 w-20 text-3xl'
})[this.size]
},
roundedClass () {
return ({
true: 'rounded-lg',
false: 'rounded-full'
})[this.rounded]
},
placeholderClass () {
return ({
true: 'bg-gray-500 dark:bg-gray-900',
false: 'bg-tw-gray-100'
})[!!this.alt]
},
avatarClass () {
return [
this.sizeClass,
this.roundedClass,
this.placeholderClass,
this.to ? 'cursor-pointer' : ''
].join(' ')
},
statusClass () {
return [
({
online: 'bg-green-400',
idle: 'bg-yellow-400',
invisible: 'bg-tw-gray-300',
donotdisturb: 'bg-red-400',
focus: 'bg-primary-500'
})[this.status],
({
xxxs: 'h-1 w-1',
xxs: 'h-1 w-1',
xs: 'h-1.5 w-1.5',
sm: 'h-2 w-2',
md: 'h-2.5 w-2.5',
lg: 'h-3 w-3',
xl: 'h-3.5 w-3.5',
'2xl': 'h-3.5 w-3.5',
'3xl': 'h-4 w-4'
})[this.size],
({
true: 'transform -translate-y-1/2 translate-x-1/2'
})[this.rounded]
].join(' ')
}
},
methods: {
goto (e) {
if (!this.to || !this.$router) { return }
e.preventDefault()
this.$router.push(this.to)
}
}
}
</script>

View File

@@ -0,0 +1,58 @@
<template>
<div class="flex">
<Avatar
v-for="(avatar, index) of displayedGroup"
:key="index"
:src="avatar.src"
class="shadow-solid -ml-1.5 first:ml-0"
:size="size"
:status="avatar.status"
/>
<Avatar
v-if="remainingGroupSize > 0"
class="shadow-solid -ml-1.5 first:ml-0 text-[10px]"
:size="size"
:text="`+${remainingGroupSize}`"
/>
</div>
</template>
<script>
import Avatar from './Avatar.vue'
export default {
components: {
Avatar
},
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
}
},
computed: {
displayedGroup () {
if (!this.max) { return this.group }
return this.group.slice(0, this.max)
},
remainingGroupSize () {
if (!this.max) { return 0 }
return this.group.length - this.max
}
}
}
</script>

View File

@@ -0,0 +1,72 @@
<template>
<span class="inline-flex items-center font-medium" :class="badgeClass">
<slot>{{ label }}</slot>
</span>
</template>
<script>
export default {
props: {
size: {
type: String,
default: 'md',
validator (value) {
return ['sm', 'md', 'lg'].includes(value)
}
},
variant: {
type: String,
default: 'primary',
validator (value) {
return ['primary', 'gray', 'red', 'orange', 'yellow', 'green', 'teal', 'blue', 'indigo', 'purple', 'pink', 'gradient'].includes(value)
}
},
pill: {
type: Boolean,
default: false
},
label: {
type: String,
default: null
}
},
computed: {
sizeClass () {
return ({
sm: 'text-xs px-2 py-0.5',
md: 'text-sm px-2.5 py-0.5 leading-4',
lg: 'text-sm px-3 py-0.5 leading-5'
})[this.size]
},
variantClass () {
return ({
primary: 'bg-primary-100 dark:bg-primary-700 text-primary-800 dark:text-primary-100',
gray: 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-100',
red: 'bg-red-100 dark:bg-red-700 text-red-800 dark:text-red-100',
orange: 'bg-orange-100 dark:bg-orange-700 text-orange-800 dark:text-orange-100',
yellow: 'bg-yellow-100 dark:bg-yellow-700 text-yellow-800 dark:text-yellow-100',
green: 'bg-green-100 dark:bg-green-700 text-green-800 dark:text-green-100',
teal: 'bg-teal-100 dark:bg-teal-700 text-teal-800 dark:text-teal-100',
blue: 'bg-blue-100 dark:bg-blue-700 text-blue-800 dark:text-blue-100',
indigo: 'bg-indigo-100 dark:bg-indigo-700 text-indigo-800 dark:text-indigo-100',
purple: 'bg-purple-100 dark:bg-purple-700 text-purple-800 dark:text-purple-100',
pink: 'bg-pink-100 dark:bg-pink-700 text-pink-800 dark:text-pink-100',
gradient: 'bg-gradient-to-r from-indigo-600 to-blue-600 text-white'
})[this.variant]
},
roundedClass () {
return ({
false: 'rounded',
true: 'rounded-full'
})[this.pill]
},
badgeClass () {
return [
this.sizeClass,
this.variantClass,
this.roundedClass
].join(' ')
}
}
}
</script>

View File

@@ -0,0 +1,319 @@
<template>
<component
:is="is"
:class="buttonClass"
:aria-label="ariaLabel"
v-bind="props"
>
<Icon v-if="isLeading" :name="iconName" :class="leadingIconClass" aria-hidden="true" />
<slot><span :class="truncate ? 'text-left break-all line-clamp-1' : ''">{{ label }}</span></slot>
<Icon v-if="isTrailing" :name="iconName" :class="trailingIconClass" aria-hidden="true" />
</component>
</template>
<script>
import Icon from './Icon'
export default {
components: {
Icon
},
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 ['', 'xxs', 'xs', 'sm', 'md', 'lg', 'xl'].includes(value)
}
},
variant: {
type: String,
default: 'primary',
validator (value) {
return ['primary', 'secondary', 'danger', 'white', 'gray', 'gray-hover', 'white-hover', 'black', 'black-hover', 'transparent', 'link', 'gradient', 'custom'].includes(value)
}
},
icon: {
type: String,
default: null
},
loadingIcon: {
type: String,
default: null
},
trailing: {
type: Boolean,
default: false
},
leading: {
type: Boolean,
default: false
},
href: {
type: String,
default: null
},
to: {
type: [String, Object],
default: null
},
target: {
type: String,
default: null
},
ariaLabel: {
type: String,
default: null
},
rounded: {
type: Boolean,
default: false
},
iconClass: {
type: String,
default: ''
},
baseClass: {
type: String,
default: 'font-medium focus:outline-none disabled:cursor-not-allowed disabled:opacity-75'
},
customClass: {
type: String,
default: null
},
square: {
type: Boolean,
default: false
},
truncate: {
type: Boolean,
default: false
},
noFocusBorder: {
type: Boolean,
default: false
},
noPadding: {
type: Boolean,
default: false
}
},
computed: {
is () {
if (this.href) {
return 'a'
} else if (this.to) {
return 'NuxtLink'
}
return 'button'
},
props () {
switch (this.is) {
case 'a':
return {
href: this.href,
target: this.target
}
case 'NuxtLink': {
return {
to: this.to
}
}
default: {
return {
disabled: this.disabled || this.loading,
type: this.type
}
}
}
},
isLeading () {
return (this.leading && this.icon) || (this.icon && !this.trailing) || (this.loading && !this.trailing)
},
isTrailing () {
return (this.trailing && this.icon) || (this.loading && this.trailing)
},
sizeClass () {
return ({
xxs: 'text-xs',
xs: 'text-xs',
sm: 'text-sm leading-4',
md: 'text-sm',
lg: 'text-base',
xl: 'text-base'
})[this.size]
},
paddingClass () {
if (this.noPadding) {
return ''
}
const isSquare = this.square || (!this.$slots.default && !this.label)
return ({
true: {
xxs: 'px-2 py-1',
xs: 'px-2.5 py-1.5',
sm: 'px-3 py-2',
md: 'px-4 py-2',
lg: 'px-4 py-2',
xl: 'px-6 py-3'
},
false: {
xxs: 'p-1',
xs: 'p-1.5',
sm: 'p-2',
md: 'p-2',
lg: 'p-2',
xl: 'p-3'
}
})[!isSquare][this.size]
},
variantClass () {
return ({
primary: 'shadow-sm border border-transparent text-white bg-primary-600 hover:bg-primary-700 disabled:bg-primary-600',
secondary: 'border border-transparent text-primary-700 bg-primary-100 hover:bg-primary-200 disabled:bg-primary-100',
danger: 'shadow-sm border border-transparent text-white bg-red-500 dark:bg-red-600 hover:bg-red-600 dark:hover:bg-red-500 disabled:bg-red-500 dark:disabled:bg-red-600',
white: 'shadow-sm border border-tw-gray-300 text-tw-gray-700 bg-tw-white hover:bg-tw-gray-50 disabled:bg-tw-white',
'white-hover': 'border border-transparent text-tw-gray-500 hover:text-tw-gray-700 focus:text-tw-gray-700 bg-transparent hover:bg-gray-100 focus:bg-gray-100 dark:hover:bg-gray-900 dark:focus:bg-gray-900 disabled:text-tw-gray-500',
gray: 'shadow-sm border border-tw-gray-300 text-tw-gray-500 hover:text-tw-gray-700 focus:text-tw-gray-700 bg-gray-50 dark:bg-gray-800 disabled:text-tw-gray-500',
'gray-hover': 'border border-transparent text-tw-gray-500 hover:text-tw-gray-700 focus:text-tw-gray-700 bg-transparent hover:bg-gray-100 focus:bg-gray-100 dark:hover:bg-gray-800 dark:focus:bg-gray-800 disabled:text-tw-gray-500',
black: 'border border-transparent text-tw-white bg-tw-gray-800 hover:bg-tw-gray-900 focus:bg-tw-gray-900',
'black-hover': 'border border-transparent text-tw-gray-500 hover:text-tw-gray-900 focus:text-tw-gray-700 bg-transparent hover:bg-white dark:hover:bg-black focus:bg-white dark:focus:bg-black',
transparent: 'border border-transparent text-tw-gray-500 hover:text-tw-gray-700 focus:text-tw-gray-700 disabled:hover:text-tw-gray-500',
link: 'border border-transparent text-primary-500 hover:text-primary-700 focus:text-primary-700',
gradient: 'shadow-sm text-white border border-transparent bg-gradient-to-r from-indigo-600 to-blue-600 hover:from-indigo-700 hover:to-blue-700',
custom: ''
})[this.variant]
},
variantFocusBorderClass () {
if (this.noFocusBorder) {
return ''
}
return ({
primary: 'focus:ring-2 focus:ring-primary-200',
secondary: 'focus:ring-2 focus:ring-primary-500',
white: 'focus:ring-1 focus:ring-primary-500 focus:border-primary-500 dark:focus:border-primary-500',
'white-hover': '',
gray: 'focus:ring-1 focus:ring-primary-500 focus:border-primary-500 dark:focus:border-primary-500',
'gray-hover': '',
link: '',
transparent: '',
custom: ''
})[this.variant]
},
blockClass () {
return ({
true: 'w-full flex justify-center items-center',
false: 'inline-flex items-center'
})[this.block]
},
roundedClass () {
return ({
true: 'rounded-full',
false: 'rounded-md'
})[this.rounded]
},
iconName () {
if (this.loading) {
return this.loadingIcon || 'heroicons-outline:refresh'
}
return this.icon
},
loadingIconClass () {
return [
({
true: 'animate-spin'
})[this.loading]
]
},
leadingIconClass () {
return [
this.iconClass,
'flex-shrink-0',
...this.loadingIconClass,
({
xxs: 'h-3.5 w-3.5',
xs: 'h-4 w-4',
sm: 'h-4 w-4',
md: 'h-5 w-5',
lg: 'h-5 w-5',
xl: 'h-5 w-5'
})[this.size || 'sm'],
({
true: {
xxs: '-ml-0.5 mr-1',
xs: '-ml-0.5 mr-1.5',
sm: '-ml-0.5 mr-2',
md: '-ml-1 mr-2',
lg: '-ml-1 mr-3',
xl: '-ml-1 mr-3'
},
false: {}
})[!!this.$slots.default || !!(this.label?.length)][this.size]
].join(' ')
},
trailingIconClass () {
return [
this.iconClass,
'flex-shrink-0',
...this.loadingIconClass,
({
xxs: 'h-3.5 w-3.5',
xs: 'h-4 w-4',
sm: 'h-4 w-4',
md: 'h-5 w-5',
lg: 'h-5 w-5',
xl: 'h-5 w-5'
})[this.size || 'sm'],
({
true: {
xxs: 'ml-1 -mr-0.5',
xs: 'ml-1.5 -mr-0.5',
sm: 'ml-2 -mr-0.5',
md: 'ml-2 -mr-1',
lg: 'ml-3 -mr-1',
xl: 'ml-3 -mr-1'
},
false: {}
})[!!this.$slots.default || !!(this.label?.length)][this.size]
].join(' ')
},
buttonClass () {
return [
this.baseClass,
this.roundedClass,
this.sizeClass,
this.paddingClass,
this.variantClass,
this.variantFocusBorderClass,
this.blockClass,
this.customClass
].filter(Boolean).join(' ')
}
}
}
</script>

View File

@@ -0,0 +1,153 @@
<template>
<Menu v-slot="{ open }" as="div" :class="wrapperClass">
<MenuButton ref="trigger" as="div">
<slot :open="open">
Open
</slot>
</MenuButton>
<div v-if="open" ref="container" :class="containerClass">
<transition
appear
enter-active-class="transition duration-100 ease-out"
enter-from-class="transform scale-95 opacity-0"
enter-to-class="transform scale-100 opacity-100"
leave-active-class="transition duration-75 ease-out"
leave-from-class="transform scale-100 opacity-100"
leave-to-class="transform scale-95 opacity-0"
>
<MenuItems :class="itemsClass" 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 }">
<Component v-bind="item" :is="(item.to && 'NuxtLink') || (item.href && 'a') || 'button'" :class="resolveItemClass({ active, disabled })" @click="onItemClick(item)">
<slot :name="item.slot" :item="item">
<Icon v-if="item.icon" :name="item.icon" :class="itemIconClass" />
{{ item.label }}
</slot>
</Component>
</MenuItem>
</div>
</MenuItems>
</transition>
</div>
</Menu>
</template>
<script>
import {
Menu,
MenuButton,
MenuItems,
MenuItem
} from '@headlessui/vue'
import Icon from '../elements/Icon'
import { classNames, usePopper } from '../../utils'
export default {
components: {
Menu,
MenuButton,
MenuItems,
MenuItem,
Icon
},
props: {
items: {
type: Array,
default: () => []
},
placement: {
type: String,
default: 'bottom-end'
},
strategy: {
type: String,
default: 'fixed'
},
wrapperClass: {
type: String,
default: 'relative inline-block text-left'
},
containerClass: {
type: String,
default: 'w-48 z-20'
},
itemsClass: {
type: String,
default: 'bg-white divide-y divide-gray-100 dark:divide-gray-700 rounded-md ring-1 ring-black ring-opacity-5'
},
itemClass: {
type: String,
default: 'group flex items-center px-4 py-2 text-sm w-full'
},
itemActiveClass: {
type: String,
default: 'bg-tw-gray-100 text-tw-gray-900'
},
itemInactiveClass: {
type: String,
default: 'text-tw-gray-700'
},
itemDisabledClass: {
type: String,
default: 'cursor-not-allowed opacity-50'
},
itemIconClass: {
type: String,
default: 'mr-3 h-5 w-5 text-tw-gray-400 group-hover:text-tw-gray-500'
}
},
setup (props) {
const [trigger, container] = usePopper({
placement: props.placement,
strategy: props.strategy,
modifiers: [{
name: 'offset',
options: {
offset: [0, 8]
}
},
{
name: 'computeStyles',
options: {
gpuAcceleration: false,
adaptive: false
}
},
{
name: 'preventOverflow',
options: {
padding: 8
}
}]
})
function resolveItemClass ({ active, disabled }) {
return classNames(
props.itemClass,
active ? props.itemActiveClass : props.itemInactiveClass,
disabled && props.itemDisabledClass
)
}
function onItemClick (item) {
if (item.disabled) {
return
}
if (item.click) {
item.click()
}
}
return {
trigger,
container,
onItemClick,
resolveItemClass
}
}
}
</script>

View File

@@ -0,0 +1,14 @@
<template>
<div :class="name" />
</template>
<script>
export default {
props: {
name: {
type: String,
required: true
}
}
}
</script>

View File

@@ -0,0 +1,40 @@
<template>
<NuxtLink v-slot="{ href, navigate }" v-bind="$props" custom>
<a
v-bind="$attrs"
:href="href"
:class="isActive ? activeClass : inactiveClass"
@click="navigate"
>
<slot v-bind="{ isActive }" />
</a>
</NuxtLink>
</template>
<script>
import { RouterLink } from 'vue-router'
export default {
name: 'Link',
props: {
...RouterLink.props,
inactiveClass: {
type: String,
default: ''
},
exact: {
type: Boolean,
default: false
}
},
computed: {
isActive () {
if (!this.exact) {
return !!this.$route.path.startsWith(this.to)
} else {
return this.$route.path === this.to || this.$route.path === `${this.to}/`
}
}
}
}
</script>

View File

@@ -0,0 +1,46 @@
<template>
<Switch
v-model="enabled"
:class="[enabled ? 'bg-primary-600' : 'bg-tw-gray-200', 'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary-200']"
>
<span :class="[enabled ? 'translate-x-5' : 'translate-x-0', 'pointer-events-none relative inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200']">
<span :class="[enabled ? 'opacity-0 ease-out duration-100' : 'opacity-100 ease-in duration-200', 'absolute inset-0 h-full w-full flex items-center justify-center transition-opacity']" aria-hidden="true">
<Icon :name="iconOff" class="h-3 w-3 text-tw-gray-400" />
</span>
<span :class="[enabled ? 'opacity-100 ease-in duration-200' : 'opacity-0 ease-out duration-100', 'absolute inset-0 h-full w-full flex items-center justify-center transition-opacity']" aria-hidden="true">
<Icon :name="iconOn" class="h-3 w-3 text-primary-600" />
</span>
</span>
</Switch>
</template>
<script setup>
import { Switch } from '@headlessui/vue'
import Icon from './Icon'
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
iconOn: {
type: String,
default: ''
},
iconOff: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:modelValue'])
const enabled = computed({
get () {
return props.modelValue
},
set (value) {
emit('update:modelValue', value)
}
})
</script>