mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-17 21:48:07 +01:00
feat: rewrite to use app config and rework docs (#143)
Co-authored-by: Daniel Roe <daniel@roe.dev> Co-authored-by: Sébastien Chopin <seb@nuxt.com>
This commit is contained in:
708
src/runtime/app.config.ts
Normal file
708
src/runtime/app.config.ts
Normal file
@@ -0,0 +1,708 @@
|
||||
// Elements
|
||||
|
||||
const avatar = {
|
||||
wrapper: 'relative inline-flex items-center justify-center',
|
||||
background: 'bg-gray-100 dark:bg-gray-800',
|
||||
rounded: 'rounded-full',
|
||||
placeholder: 'text-xs font-medium leading-none text-gray-900 dark:text-white truncate',
|
||||
size: {
|
||||
'3xs': 'h-4 w-4 text-xs',
|
||||
'2xs': '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'
|
||||
},
|
||||
chip: {
|
||||
base: 'absolute block rounded-full ring-2 ring-white dark:ring-gray-900',
|
||||
position: {
|
||||
'top-right': 'top-0 right-0',
|
||||
'bottom-right': 'bottom-0 right-0',
|
||||
'top-left': 'top-0 left-0',
|
||||
'bottom-left': 'bottom-0 left-0'
|
||||
},
|
||||
variant: {
|
||||
solid: 'bg-{color}-400'
|
||||
},
|
||||
size: {
|
||||
'3xs': 'h-1 w-1',
|
||||
'2xs': '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'
|
||||
}
|
||||
},
|
||||
default: {
|
||||
size: 'md',
|
||||
chipVariant: 'solid',
|
||||
chipPosition: 'top-right'
|
||||
}
|
||||
}
|
||||
|
||||
const avatarGroup = {
|
||||
wrapper: 'flex flex-row-reverse',
|
||||
ring: 'ring-2 ring-white dark:ring-gray-900',
|
||||
margin: '-mr-1.5 first:mr-0'
|
||||
}
|
||||
|
||||
const badge = {
|
||||
base: 'inline-flex items-center',
|
||||
rounded: 'rounded-md',
|
||||
font: 'font-medium',
|
||||
size: {
|
||||
sm: 'text-xs px-1.5 py-0.5',
|
||||
md: 'text-xs px-2 py-1',
|
||||
lg: 'text-xs px-2.5 py-1.5'
|
||||
},
|
||||
variant: {
|
||||
solid: 'bg-{color}-50 dark:bg-{color}-400 dark:bg-opacity-10 text-{color}-500 dark:text-{color}-400 ring-1 ring-inset ring-{color}-500 dark:ring-{color}-400 ring-opacity-10 dark:ring-opacity-20'
|
||||
},
|
||||
default: {
|
||||
size: 'md',
|
||||
variant: 'solid',
|
||||
color: 'primary'
|
||||
}
|
||||
}
|
||||
|
||||
const button = {
|
||||
base: 'focus:outline-none focus-visible:outline-0 disabled:cursor-not-allowed disabled:opacity-75',
|
||||
font: 'font-medium',
|
||||
rounded: 'rounded-md',
|
||||
size: {
|
||||
'2xs': 'text-xs',
|
||||
xs: 'text-xs',
|
||||
sm: 'text-sm',
|
||||
md: 'text-sm',
|
||||
lg: 'text-base',
|
||||
xl: 'text-base'
|
||||
},
|
||||
gap: {
|
||||
'2xs': 'gap-x-1',
|
||||
xs: 'gap-x-1.5',
|
||||
sm: 'gap-x-2',
|
||||
md: 'gap-x-2',
|
||||
lg: 'gap-x-2',
|
||||
xl: 'gap-x-2'
|
||||
},
|
||||
spacing: {
|
||||
'2xs': 'px-2 py-1',
|
||||
xs: 'px-2.5 py-1.5',
|
||||
sm: 'px-3 py-1.5',
|
||||
md: 'px-3 py-2',
|
||||
lg: 'px-4 py-2',
|
||||
xl: 'px-4 py-3'
|
||||
},
|
||||
square: {
|
||||
'2xs': 'p-[5px]',
|
||||
xs: 'p-1.5',
|
||||
sm: 'p-2',
|
||||
md: 'p-2',
|
||||
lg: 'p-2.5',
|
||||
xl: 'p-3'
|
||||
},
|
||||
color: {
|
||||
white: {
|
||||
solid: 'shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-700 text-gray-900 dark:text-white bg-white hover:bg-gray-50 disabled:bg-white dark:bg-gray-900 dark:hover:bg-gray-800/50 dark:disabled:bg-gray-900 focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400',
|
||||
ghost: 'text-gray-900 dark:text-white hover:bg-white dark:hover:bg-gray-900 focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400'
|
||||
},
|
||||
gray: {
|
||||
solid: 'shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-700 text-gray-700 dark:text-gray-200 bg-gray-50 hover:bg-gray-100 disabled:bg-gray-50 dark:bg-gray-800 dark:hover:bg-gray-700/50 dark:disabled:bg-gray-800 focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400',
|
||||
// TODO: For Volta
|
||||
// 'outline-ghost': 'text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800 hover:ring-1 ring-inset ring-gray-300 dark:ring-gray-700',
|
||||
ghost: 'text-gray-700 dark:text-gray-200 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-800 focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400',
|
||||
link: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 underline-offset-4 hover:underline focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400'
|
||||
},
|
||||
black: {
|
||||
solid: 'shadow-sm text-white dark:text-gray-900 bg-gray-900 hover:bg-gray-800 disabled:bg-gray-900 dark:bg-white dark:hover:bg-gray-100 dark:disabled:bg-white focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400',
|
||||
link: 'text-gray-900 dark:text-white underline-offset-4 hover:underline focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400'
|
||||
}
|
||||
},
|
||||
variant: {
|
||||
solid: 'shadow-sm text-white dark:text-gray-900 bg-{color}-500 hover:bg-{color}-600 disabled:bg-{color}-500 dark:bg-{color}-400 dark:hover:bg-{color}-500 dark:disabled:bg-{color}-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-{color}-500 dark:focus-visible:outline-{color}-400',
|
||||
outline: 'ring-1 ring-inset ring-current text-{color}-500 dark:text-{color}-400 hover:bg-{color}-50 dark:hover:bg-{color}-950 focus-visible:ring-2 focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400',
|
||||
soft: 'text-{color}-500 dark:text-{color}-400 bg-{color}-50 hover:bg-{color}-100 dark:bg-{color}-950 dark:hover:bg-{color}-900 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400',
|
||||
ghost: 'text-{color}-500 dark:text-{color}-400 hover:bg-{color}-50 dark:hover:bg-{color}-950 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400',
|
||||
link: 'text-{color}-500 hover:text-{color}-600 dark:text-{color}-400 dark:hover:text-{color}-500 underline-offset-4 hover:underline focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400'
|
||||
},
|
||||
icon: {
|
||||
base: 'flex-shrink-0',
|
||||
size: {
|
||||
'2xs': '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-6 w-6'
|
||||
}
|
||||
},
|
||||
default: {
|
||||
size: 'sm',
|
||||
variant: 'solid',
|
||||
color: 'primary',
|
||||
loadingIcon: 'i-heroicons-arrow-path-20-solid'
|
||||
}
|
||||
}
|
||||
|
||||
const buttonGroup = {
|
||||
wrapper: 'inline-flex',
|
||||
rounded: 'rounded-md',
|
||||
shadow: 'shadow-sm'
|
||||
}
|
||||
|
||||
const dropdown = {
|
||||
wrapper: 'relative inline-flex text-left',
|
||||
container: 'z-20',
|
||||
width: 'w-48',
|
||||
background: 'bg-white dark:bg-gray-800',
|
||||
shadow: 'shadow-lg',
|
||||
rounded: 'rounded-md',
|
||||
ring: 'ring-1 ring-gray-200 dark:ring-gray-700',
|
||||
base: 'focus:outline-none',
|
||||
divide: 'divide-y divide-gray-200 dark:divide-gray-700',
|
||||
spacing: 'p-1',
|
||||
item: {
|
||||
base: 'group flex items-center gap-2 px-2 py-1.5 text-sm w-full rounded-md',
|
||||
active: 'bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-white',
|
||||
inactive: 'text-gray-700 dark:text-gray-200',
|
||||
disabled: 'cursor-not-allowed opacity-50',
|
||||
icon: {
|
||||
base: 'flex-shrink-0 h-4 w-4',
|
||||
active: 'text-gray-500 dark:text-gray-400',
|
||||
inactive: 'text-gray-400 dark:text-gray-500'
|
||||
},
|
||||
avatar: {
|
||||
base: 'flex-shrink-0',
|
||||
size: '3xs'
|
||||
},
|
||||
shortcuts: 'hidden md:inline-flex flex-shrink-0 text-xs font-semibold text-gray-500 dark:text-gray-400 ml-auto'
|
||||
},
|
||||
transition: {
|
||||
enterActiveClass: 'transition duration-100 ease-out',
|
||||
enterFromClass: 'transform scale-95 opacity-0',
|
||||
enterToClass: 'transform scale-100 opacity-100',
|
||||
leaveActiveClass: 'transition duration-75 ease-out',
|
||||
leaveFromClass: 'transform scale-100 opacity-100',
|
||||
leaveToClass: 'transform scale-95 opacity-0'
|
||||
},
|
||||
popper: {
|
||||
placement: 'bottom-end',
|
||||
strategy: 'fixed'
|
||||
}
|
||||
}
|
||||
|
||||
// Forms
|
||||
|
||||
const input = {
|
||||
wrapper: 'relative',
|
||||
base: 'relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none',
|
||||
custom: '',
|
||||
size: {
|
||||
'2xs': 'text-xs',
|
||||
xs: 'text-xs',
|
||||
sm: 'text-sm',
|
||||
md: 'text-sm',
|
||||
lg: 'text-base',
|
||||
xl: 'text-base'
|
||||
},
|
||||
gap: {
|
||||
'2xs': 'gap-x-1',
|
||||
xs: 'gap-x-1.5',
|
||||
sm: 'gap-x-2',
|
||||
md: 'gap-x-2',
|
||||
lg: 'gap-x-2',
|
||||
xl: 'gap-x-2'
|
||||
},
|
||||
spacing: {
|
||||
'2xs': 'px-2 py-1',
|
||||
xs: 'px-2.5 py-1.5',
|
||||
sm: 'px-3 py-1.5',
|
||||
md: 'px-3 py-2',
|
||||
lg: 'px-4 py-2',
|
||||
xl: 'px-4 py-3'
|
||||
},
|
||||
leading: {
|
||||
spacing: {
|
||||
'2xs': 'pl-[26px]',
|
||||
xs: 'pl-8',
|
||||
sm: 'pl-9',
|
||||
md: 'pl-10',
|
||||
lg: 'pl-11',
|
||||
xl: 'pl-12'
|
||||
}
|
||||
},
|
||||
trailing: {
|
||||
spacing: {
|
||||
'2xs': 'pr-[26px]',
|
||||
xs: 'pr-8',
|
||||
sm: 'pr-9',
|
||||
md: 'pr-10',
|
||||
lg: 'pr-11',
|
||||
xl: 'pr-12'
|
||||
}
|
||||
},
|
||||
appearance: {
|
||||
white: 'border-0 bg-white dark:bg-gray-900 text-gray-900 dark:text-white rounded-md shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400 placeholder:text-gray-400 dark:placeholder:text-gray-500',
|
||||
gray: 'border-0 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white rounded-md shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400 placeholder:text-gray-400 dark:placeholder:text-gray-500',
|
||||
none: 'border-0 bg-transparent focus:ring-0 focus:shadow-none'
|
||||
},
|
||||
icon: {
|
||||
base: 'text-gray-400 dark:text-gray-500',
|
||||
size: {
|
||||
'2xs': '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-6 w-6'
|
||||
},
|
||||
leading: {
|
||||
wrapper: 'absolute inset-y-0 left-0 flex items-center pointer-events-none',
|
||||
spacing: {
|
||||
'2xs': 'pl-2',
|
||||
xs: 'pl-2.5',
|
||||
sm: 'pl-3',
|
||||
md: 'pl-3',
|
||||
lg: 'pl-4',
|
||||
xl: 'pl-4'
|
||||
}
|
||||
},
|
||||
trailing: {
|
||||
wrapper: 'absolute inset-y-0 right-0 flex items-center pointer-events-none',
|
||||
spacing: {
|
||||
'2xs': 'pr-2',
|
||||
xs: 'pr-2.5',
|
||||
sm: 'pr-3',
|
||||
md: 'pr-3',
|
||||
lg: 'pr-4',
|
||||
xl: 'pr-4'
|
||||
}
|
||||
}
|
||||
},
|
||||
default: {
|
||||
size: 'sm',
|
||||
appearance: 'white',
|
||||
loadingIcon: 'i-heroicons-arrow-path-20-solid'
|
||||
}
|
||||
}
|
||||
|
||||
const inputGroup = {
|
||||
wrapper: '',
|
||||
label: 'block text-sm font-medium text-gray-700 dark:text-gray-200',
|
||||
labelWrapper: 'flex content-center justify-between',
|
||||
container: 'mt-1 relative',
|
||||
required: 'text-red-400',
|
||||
description: 'text-sm leading-5 text-gray-500 dark:text-gray-400',
|
||||
hint: 'text-sm leading-5 text-gray-500 dark:text-gray-400',
|
||||
help: 'mt-2 text-sm text-gray-500 dark:text-gray-400'
|
||||
}
|
||||
|
||||
const textarea = {
|
||||
...input
|
||||
}
|
||||
|
||||
const select = {
|
||||
...input
|
||||
}
|
||||
|
||||
const selectMenu = {
|
||||
wrapper: 'relative',
|
||||
container: 'z-20',
|
||||
width: 'w-full',
|
||||
height: 'max-h-60',
|
||||
base: 'relative focus:outline-none overflow-y-auto scroll-py-1',
|
||||
background: 'bg-white dark:bg-gray-800',
|
||||
shadow: 'shadow-lg',
|
||||
rounded: 'rounded-md',
|
||||
spacing: 'p-1',
|
||||
ring: 'ring-1 ring-gray-200 dark:ring-gray-700',
|
||||
input: 'block w-[calc(100%+0.5rem)] focus:ring-transparent text-sm px-3 py-1.5 u-text-gray-700 bg-white dark:bg-gray-800 border-0 border-b border-gray-200 dark:border-gray-700 focus:border-inherit sticky -top-1 -mt-1 mb-1 -mx-1 z-10 placeholder-gray-400 dark:placeholder-gray-500',
|
||||
option: {
|
||||
base: 'cursor-default select-none relative px-2 py-1.5 rounded-md text-sm text-gray-900 dark:text-white flex items-center justify-between gap-1',
|
||||
container: 'flex items-center gap-2',
|
||||
active: 'bg-gray-100 dark:bg-gray-900',
|
||||
inactive: '',
|
||||
disabled: 'cursor-not-allowed opacity-50',
|
||||
empty: 'text-sm text-gray-400 dark:text-gray-500 px-2 py-1.5',
|
||||
icon: {
|
||||
base: 'flex-shrink-0 h-4 w-4',
|
||||
active: 'text-gray-900 dark:text-white',
|
||||
inactive: 'text-gray-400 dark:text-gray-500'
|
||||
},
|
||||
avatar: {
|
||||
base: 'flex-shrink-0',
|
||||
size: '3xs'
|
||||
},
|
||||
chip: {
|
||||
base: 'flex-shrink-0 w-2 h-2 mx-1 rounded-full'
|
||||
},
|
||||
selected: {
|
||||
wrapper: 'absolute inset-y-0 right-0 flex items-center pr-2',
|
||||
icon: 'h-4 w-4 text-gray-900 dark:text-white flex-shrink-0'
|
||||
}
|
||||
},
|
||||
transition: {
|
||||
leaveActiveClass: 'transition ease-in duration-100',
|
||||
leaveFromClass: 'opacity-100',
|
||||
leaveToClass: 'opacity-0'
|
||||
},
|
||||
popper: {
|
||||
placement: 'bottom-end'
|
||||
},
|
||||
default: {
|
||||
selectedIcon: 'i-heroicons-check-20-solid'
|
||||
}
|
||||
}
|
||||
|
||||
const radio = {
|
||||
wrapper: 'relative flex items-start',
|
||||
base: 'h-4 w-4 text-primary-500 dark:text-primary-400 focus:ring-2 focus:ring-offset-2 bg-white dark:bg-gray-900 dark:checked:bg-current dark:checked:border-transparent focus:ring-primary-500 dark:focus:ring-primary-400 focus:ring-offset-white dark:focus:ring-offset-gray-900 border-gray-300 dark:border-gray-700 disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
label: 'font-medium text-gray-700 dark:text-gray-200',
|
||||
required: 'text-red-400',
|
||||
help: 'text-gray-500 dark:text-gray-400'
|
||||
}
|
||||
|
||||
const checkbox = {
|
||||
...radio,
|
||||
base: radio.base + ' rounded'
|
||||
}
|
||||
|
||||
const toggle = {
|
||||
base: '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-offset-2 focus:ring-primary-500 dark:focus:ring-primary-400 focus:ring-offset-white dark:focus:ring-offset-gray-900',
|
||||
active: 'bg-primary-500 dark:bg-primary-400',
|
||||
inactive: 'bg-gray-200 dark:bg-gray-700',
|
||||
container: {
|
||||
base: 'pointer-events-none relative inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200',
|
||||
active: 'translate-x-5',
|
||||
inactive: 'translate-x-0'
|
||||
},
|
||||
icon: {
|
||||
base: 'absolute inset-0 h-full w-full flex items-center justify-center transition-opacity',
|
||||
active: 'opacity-100 ease-in duration-200',
|
||||
inactive: 'opacity-0 ease-out duration-100',
|
||||
on: 'h-3 w-3 text-primary-500 dark:text-primary-400',
|
||||
off: 'h-3 w-3 text-gray-400 dark:text-gray-500'
|
||||
}
|
||||
}
|
||||
|
||||
// Layout
|
||||
|
||||
const card = {
|
||||
base: 'overflow-hidden',
|
||||
background: 'bg-white dark:bg-gray-900',
|
||||
divide: 'divide-y divide-gray-200 dark:divide-gray-700',
|
||||
ring: 'ring-1 ring-gray-200 dark:ring-gray-700',
|
||||
rounded: 'rounded-lg',
|
||||
shadow: 'shadow',
|
||||
body: {
|
||||
base: '',
|
||||
background: '',
|
||||
spacing: 'px-4 py-5 sm:p-6'
|
||||
},
|
||||
header: {
|
||||
base: '',
|
||||
background: '',
|
||||
spacing: 'px-4 py-5 sm:px-6'
|
||||
},
|
||||
footer: {
|
||||
base: '',
|
||||
background: '',
|
||||
spacing: 'px-4 py-4 sm:px-6'
|
||||
}
|
||||
}
|
||||
|
||||
const container = {
|
||||
base: 'mx-auto',
|
||||
spacing: 'px-4 sm:px-6 lg:px-8',
|
||||
constrained: 'max-w-7xl'
|
||||
}
|
||||
|
||||
// Navigation
|
||||
|
||||
const verticalNavigation = {
|
||||
wrapper: 'relative z-0',
|
||||
base: 'group flex items-center gap-2 text-sm font-medium rounded-md w-full relative focus:outline-none after:absolute after:inset-px after:z-[-1] after:rounded-md disabled:cursor-not-allowed disabled:opacity-75',
|
||||
spacing: 'px-3 py-1.5',
|
||||
active: 'u-text-gray-900 after:bg-gray-100 dark:after:bg-gray-800',
|
||||
inactive: 'u-text-gray-500 hover:u-text-gray-900 hover:after:bg-gray-50 dark:hover:after:bg-gray-800/50 focus-visible:after:bg-gray-50 dark:focus-visible:after:bg-gray-800/50',
|
||||
icon: {
|
||||
base: 'flex-shrink-0 w-4 h-4',
|
||||
active: 'u-text-gray-700',
|
||||
inactive: 'u-text-gray-400 group-hover:u-text-gray-700'
|
||||
},
|
||||
avatar: {
|
||||
base: 'flex-shrink-0',
|
||||
size: '3xs'
|
||||
},
|
||||
badge: {
|
||||
base: 'ml-auto inline-block py-0.5 px-2 text-xs rounded-md -mr-1 -my-0.5',
|
||||
active: 'bg-white dark:bg-gray-900',
|
||||
inactive: 'u-bg-gray-100 u-text-gray-600 group-hover:bg-white dark:group-hover:bg-gray-900'
|
||||
}
|
||||
}
|
||||
|
||||
const commandPalette = {
|
||||
wrapper: 'flex flex-col flex-1 min-h-0 divide-y divide-gray-100 dark:divide-gray-800',
|
||||
container: 'relative flex-1 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-800 scroll-py-2',
|
||||
input: {
|
||||
wrapper: 'relative flex items-center',
|
||||
base: 'w-full h-12 px-4 placeholder-gray-400 dark:placeholder-gray-500 bg-transparent border-0 text-gray-900 dark:text-white focus:ring-0 sm:text-sm',
|
||||
spacing: 'pl-10',
|
||||
icon: 'pointer-events-none absolute left-4 h-4 w-4 text-gray-400 dark:text-gray-500',
|
||||
close: 'absolute right-4'
|
||||
},
|
||||
empty: {
|
||||
wrapper: 'flex flex-col items-center justify-center flex-1 px-6 py-14 sm:px-14',
|
||||
label: 'text-sm text-center text-gray-900 dark:text-white',
|
||||
queryLabel: 'text-sm text-center text-gray-900 dark:text-white',
|
||||
icon: 'w-6 h-6 mx-auto text-gray-400 dark:text-gray-500 mb-4'
|
||||
},
|
||||
group: {
|
||||
wrapper: 'p-2',
|
||||
label: 'px-2 my-2 text-xs font-semibold text-gray-900 dark:text-white',
|
||||
container: 'text-sm text-gray-700 dark:text-gray-200',
|
||||
command: {
|
||||
base: 'flex justify-between select-none items-center rounded-md px-2 py-1.5 gap-2 relative',
|
||||
active: 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white',
|
||||
inactive: '',
|
||||
label: 'flex items-center gap-1.5 min-w-0',
|
||||
prefix: 'text-gray-400 dark:text-gray-500',
|
||||
suffix: 'text-gray-400 dark:text-gray-500',
|
||||
container: 'flex items-center gap-2 min-w-0',
|
||||
icon: {
|
||||
base: 'flex-shrink-0 w-4 h-4',
|
||||
active: 'text-gray-900 dark:text-white',
|
||||
inactive: 'text-gray-400 dark:text-gray-500'
|
||||
},
|
||||
avatar: {
|
||||
base: 'flex-shrink-0',
|
||||
size: '3xs'
|
||||
},
|
||||
chip: {
|
||||
base: 'flex-shrink-0 w-2 h-2 mx-1 rounded-full'
|
||||
},
|
||||
disabled: 'opacity-50',
|
||||
selected: {
|
||||
icon: 'h-4 w-4 text-gray-900 dark:text-white flex-shrink-0'
|
||||
},
|
||||
shortcuts: 'hidden md:inline-flex flex-shrink-0 text-xs font-semibold text-gray-500 dark:text-gray-400'
|
||||
},
|
||||
active: 'flex-shrink-0 text-gray-500 dark:text-gray-400',
|
||||
inactive: 'flex-shrink-0 text-gray-500 dark:text-gray-400'
|
||||
},
|
||||
default: {
|
||||
icon: 'i-heroicons-magnifying-glass-20-solid',
|
||||
empty: {
|
||||
icon: 'i-heroicons-magnifying-glass-20-solid',
|
||||
label: 'We couldn\'t find any items.',
|
||||
queryLabel: 'We couldn\'t find any items with that term. Please try again.'
|
||||
},
|
||||
close: null,
|
||||
selectedIcon: 'i-heroicons-check-20-solid'
|
||||
}
|
||||
}
|
||||
|
||||
// Overlays
|
||||
|
||||
const modal = {
|
||||
wrapper: 'relative z-50',
|
||||
inner: 'fixed inset-0 overflow-y-auto',
|
||||
container: 'flex min-h-full items-end sm:items-center justify-center text-center',
|
||||
spacing: 'p-4 sm:p-0',
|
||||
base: 'relative text-left overflow-hidden sm:my-8 w-full flex flex-col',
|
||||
overlay: {
|
||||
base: 'fixed inset-0 transition-opacity',
|
||||
background: 'bg-gray-500/75 dark:bg-gray-600/75',
|
||||
transition: {
|
||||
enter: 'ease-out duration-300',
|
||||
enterFrom: 'opacity-0',
|
||||
enterTo: 'opacity-100',
|
||||
leave: 'ease-in duration-200',
|
||||
leaveFrom: 'opacity-100',
|
||||
leaveTo: 'opacity-0'
|
||||
}
|
||||
},
|
||||
background: 'bg-white dark:bg-gray-900',
|
||||
ring: '',
|
||||
rounded: 'rounded-lg',
|
||||
shadow: 'shadow-xl',
|
||||
width: 'sm:max-w-lg',
|
||||
height: '',
|
||||
transition: {
|
||||
enter: 'ease-out duration-300',
|
||||
enterFrom: 'opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95',
|
||||
enterTo: 'opacity-100 translate-y-0 sm:scale-100',
|
||||
leave: 'ease-in duration-200',
|
||||
leaveFrom: 'opacity-100 translate-y-0 sm:scale-100',
|
||||
leaveTo: 'opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95'
|
||||
}
|
||||
}
|
||||
|
||||
const slideover = {
|
||||
wrapper: 'fixed inset-0 flex z-50',
|
||||
overlay: {
|
||||
base: 'fixed inset-0 transition-opacity',
|
||||
background: 'bg-gray-500/75 dark:bg-gray-600/75',
|
||||
transition: {
|
||||
enter: 'ease-in-out duration-500',
|
||||
enterFrom: 'opacity-0',
|
||||
enterTo: 'opacity-100',
|
||||
leave: 'ease-in-out duration-500',
|
||||
leaveFrom: 'opacity-100',
|
||||
leaveTo: 'opacity-0'
|
||||
}
|
||||
},
|
||||
base: 'relative flex-1 flex flex-col w-full focus:outline-none',
|
||||
background: 'bg-white dark:bg-gray-900',
|
||||
ring: '',
|
||||
rounded: '',
|
||||
shadow: 'shadow-xl',
|
||||
width: 'w-screen max-w-md',
|
||||
transition: {
|
||||
enter: 'transform transition ease-in-out duration-500 sm:duration-700',
|
||||
leave: 'transform transition ease-in-out duration-500 sm:duration-700'
|
||||
}
|
||||
}
|
||||
|
||||
const tooltip = {
|
||||
wrapper: 'relative inline-flex',
|
||||
container: 'z-20',
|
||||
width: 'max-w-xs',
|
||||
background: 'bg-white dark:bg-gray-900',
|
||||
shadow: 'shadow',
|
||||
rounded: 'rounded',
|
||||
ring: 'ring-1 ring-gray-200 dark:ring-gray-800',
|
||||
base: 'invisible lg:visible h-6 px-2 py-1 text-xs font-normal truncate',
|
||||
shortcuts: 'hidden md:inline-flex items-center justify-end flex-shrink-0 gap-0.5 ml-1',
|
||||
transition: {
|
||||
enterActiveClass: 'transition ease-out duration-200',
|
||||
enterFromClass: 'opacity-0 translate-y-1',
|
||||
enterToClass: 'opacity-100 translate-y-0',
|
||||
leaveActiveClass: 'transition ease-in duration-150',
|
||||
leaveFromClass: 'opacity-100 translate-y-0',
|
||||
leaveToClass: 'opacity-0 translate-y-1'
|
||||
},
|
||||
popper: {
|
||||
strategy: 'fixed'
|
||||
}
|
||||
}
|
||||
|
||||
const popover = {
|
||||
wrapper: 'relative',
|
||||
container: 'z-20',
|
||||
width: '',
|
||||
background: 'bg-white dark:bg-gray-900',
|
||||
shadow: 'shadow-lg',
|
||||
rounded: 'rounded-md',
|
||||
ring: 'ring-1 ring-gray-200 dark:ring-gray-800',
|
||||
base: 'overflow-hidden focus:outline-none',
|
||||
transition: {
|
||||
enterActiveClass: 'transition ease-out duration-200',
|
||||
enterFromClass: 'opacity-0 translate-y-1',
|
||||
enterToClass: 'opacity-100 translate-y-0',
|
||||
leaveActiveClass: 'transition ease-in duration-150',
|
||||
leaveFromClass: 'opacity-100 translate-y-0',
|
||||
leaveToClass: 'opacity-0 translate-y-1'
|
||||
},
|
||||
popper: {
|
||||
strategy: 'fixed'
|
||||
}
|
||||
}
|
||||
|
||||
const contextMenu = {
|
||||
wrapper: 'relative',
|
||||
container: 'z-20',
|
||||
width: '',
|
||||
background: 'bg-white dark:bg-gray-900',
|
||||
shadow: 'shadow-lg',
|
||||
rounded: 'rounded-md',
|
||||
ring: 'ring-1 ring-gray-200 dark:ring-gray-800',
|
||||
base: 'overflow-hidden focus:outline-none',
|
||||
transition: {
|
||||
enterActiveClass: 'transition ease-out duration-200',
|
||||
enterFromClass: 'opacity-0 translate-y-1',
|
||||
enterToClass: 'opacity-100 translate-y-0',
|
||||
leaveActiveClass: 'transition ease-in duration-150',
|
||||
leaveFromClass: 'opacity-100 translate-y-0',
|
||||
leaveToClass: 'opacity-0 translate-y-1'
|
||||
},
|
||||
popper: {
|
||||
placement: 'bottom-start',
|
||||
scroll: false
|
||||
}
|
||||
}
|
||||
|
||||
const notification = {
|
||||
wrapper: 'w-full pointer-events-auto',
|
||||
container: 'relative overflow-hidden',
|
||||
title: 'text-sm font-medium text-gray-900 dark:text-white',
|
||||
description: 'mt-1 text-sm leading-5 text-gray-500 dark:text-gray-400',
|
||||
background: 'bg-white dark:bg-gray-900',
|
||||
shadow: 'shadow-lg',
|
||||
rounded: 'rounded-lg',
|
||||
ring: 'ring-1 ring-gray-200 dark:ring-gray-800',
|
||||
icon: 'flex-shrink-0 w-5 h-5 text-gray-900 dark:text-white',
|
||||
avatar: 'flex-shrink-0 pt-0.5',
|
||||
progress: 'absolute bottom-0 left-0 right-0 h-1 bg-primary-500 dark:bg-primary-400',
|
||||
transition: {
|
||||
enterActiveClass: 'transform ease-out duration-300 transition',
|
||||
enterFromClass: 'translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2',
|
||||
enterToClass: 'translate-y-0 opacity-100 sm:translate-x-0',
|
||||
leaveActiveClass: 'transition ease-in duration-100',
|
||||
leaveFromClass: 'opacity-100',
|
||||
leaveToClass: 'opacity-0'
|
||||
},
|
||||
default: {
|
||||
close: {
|
||||
icon: 'i-heroicons-x-mark-20-solid',
|
||||
color: 'gray',
|
||||
variant: 'link',
|
||||
padded: false
|
||||
},
|
||||
action: {
|
||||
size: 'xs',
|
||||
color: 'white'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const notifications = {
|
||||
wrapper: 'fixed bottom-0 right-0 flex flex-col justify-end w-full z-[55] sm:w-96',
|
||||
container: 'px-4 sm:px-6 py-6 space-y-3 overflow-y-auto'
|
||||
}
|
||||
|
||||
export default {
|
||||
ui: {
|
||||
avatar,
|
||||
avatarGroup,
|
||||
badge,
|
||||
button,
|
||||
buttonGroup,
|
||||
dropdown,
|
||||
input,
|
||||
inputGroup,
|
||||
textarea,
|
||||
select,
|
||||
selectMenu,
|
||||
checkbox,
|
||||
radio,
|
||||
toggle,
|
||||
card,
|
||||
container,
|
||||
verticalNavigation,
|
||||
commandPalette,
|
||||
modal,
|
||||
slideover,
|
||||
popover,
|
||||
tooltip,
|
||||
contextMenu,
|
||||
notification,
|
||||
notifications
|
||||
}
|
||||
}
|
||||
@@ -1,123 +1,135 @@
|
||||
<template>
|
||||
<span :class="wrapperClass">
|
||||
<img v-if="url && !error" :class="avatarClass" :src="url" :alt="alt" :onerror="() => onError()">
|
||||
<span v-else-if="text || placeholder" :class="placeholderClass">{{ text || placeholder }}</span>
|
||||
<span v-else-if="text || placeholder" :class="ui.placeholder">{{ text || placeholder }}</span>
|
||||
|
||||
<span v-if="chip" :class="chipClass" />
|
||||
<span v-if="chipColor" :class="chipClass" />
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
const props = defineProps({
|
||||
src: {
|
||||
type: [String, Boolean],
|
||||
default: null
|
||||
},
|
||||
alt: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.avatar.size).includes(value)
|
||||
}
|
||||
},
|
||||
rounded: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
chip: {
|
||||
type: String,
|
||||
default: null,
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.avatar.chip.variant).includes(value)
|
||||
}
|
||||
},
|
||||
chipPosition: {
|
||||
type: String,
|
||||
default: 'top-right',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.avatar.chip.position).includes(value)
|
||||
}
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.avatar.wrapper
|
||||
},
|
||||
backgroundClass: {
|
||||
type: String,
|
||||
default: () => $ui.avatar.background
|
||||
},
|
||||
placeholderClass: {
|
||||
type: String,
|
||||
default: () => $ui.avatar.placeholder
|
||||
},
|
||||
roundedClass: {
|
||||
type: String,
|
||||
default: () => $ui.avatar.rounded
|
||||
}
|
||||
})
|
||||
|
||||
const wrapperClass = computed(() => {
|
||||
return classNames(
|
||||
props.wrapperClass,
|
||||
props.backgroundClass,
|
||||
$ui.avatar.size[props.size],
|
||||
props.rounded ? 'rounded-full' : props.roundedClass
|
||||
)
|
||||
})
|
||||
|
||||
const avatarClass = computed(() => {
|
||||
return classNames(
|
||||
$ui.avatar.size[props.size],
|
||||
props.rounded ? 'rounded-full' : props.roundedClass
|
||||
)
|
||||
})
|
||||
|
||||
const chipClass = computed(() => {
|
||||
return classNames(
|
||||
$ui.avatar.chip.base,
|
||||
$ui.avatar.chip.variant[props.chip],
|
||||
$ui.avatar.chip.position[props.chipPosition],
|
||||
$ui.avatar.chip.size[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
const url = computed(() => {
|
||||
if (typeof props.src === 'boolean') {
|
||||
return null
|
||||
}
|
||||
return props.src
|
||||
})
|
||||
|
||||
const placeholder = computed(() => {
|
||||
return (props.alt || '').split(' ').map(word => word.charAt(0)).join('').substring(0, 2)
|
||||
})
|
||||
|
||||
const error = ref(false)
|
||||
|
||||
watch(() => props.src, () => {
|
||||
if (error.value) {
|
||||
error.value = false
|
||||
}
|
||||
})
|
||||
|
||||
function onError () {
|
||||
error.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UAvatar' }
|
||||
import { defineComponent, ref, computed, watch } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { classNames } from '../../utils'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
src: {
|
||||
type: [String, Boolean],
|
||||
default: null
|
||||
},
|
||||
alt: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.avatar.default.size,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.avatar.size).includes(value)
|
||||
}
|
||||
},
|
||||
chipColor: {
|
||||
type: String,
|
||||
default: null,
|
||||
validator (value: string) {
|
||||
return appConfig.ui.colors.includes(value)
|
||||
}
|
||||
},
|
||||
chipVariant: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.avatar.default.chipVariant,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.avatar.chip.variant).includes(value)
|
||||
}
|
||||
},
|
||||
chipPosition: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.avatar.default.chipPosition,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.avatar.chip.position).includes(value)
|
||||
}
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.avatar>>,
|
||||
default: () => appConfig.ui.avatar
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.avatar>>(() => defu({}, props.ui, appConfig.ui.avatar))
|
||||
|
||||
const wrapperClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.wrapper,
|
||||
ui.value.background,
|
||||
ui.value.rounded,
|
||||
ui.value.size[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
const avatarClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.rounded,
|
||||
ui.value.size[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
const chipClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.chip.base,
|
||||
ui.value.chip.size[props.size],
|
||||
ui.value.chip.position[props.chipPosition],
|
||||
ui.value.chip.variant[props.chipVariant]?.replaceAll('{color}', props.chipColor)
|
||||
)
|
||||
})
|
||||
|
||||
const url = computed(() => {
|
||||
if (typeof props.src === 'boolean') {
|
||||
return null
|
||||
}
|
||||
return props.src
|
||||
})
|
||||
|
||||
const placeholder = computed(() => {
|
||||
return (props.alt || '').split(' ').map(word => word.charAt(0)).join('').substring(0, 2)
|
||||
})
|
||||
|
||||
const error = ref(false)
|
||||
|
||||
watch(() => props.src, () => {
|
||||
if (error.value) {
|
||||
error.value = false
|
||||
}
|
||||
})
|
||||
|
||||
function onError () {
|
||||
error.value = true
|
||||
}
|
||||
|
||||
return {
|
||||
wrapperClass,
|
||||
avatarClass,
|
||||
chipClass,
|
||||
url,
|
||||
placeholder,
|
||||
error,
|
||||
onError
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
80
src/runtime/components/elements/AvatarGroup.ts
Normal file
80
src/runtime/components/elements/AvatarGroup.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { h, computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { classNames } from '../../utils'
|
||||
import Avatar from './Avatar.vue'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
size: {
|
||||
type: String,
|
||||
default: null,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.avatar.size).includes(value)
|
||||
}
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.avatarGroup>>,
|
||||
default: () => appConfig.ui.avatarGroup
|
||||
}
|
||||
},
|
||||
setup (props, { slots }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.avatarGroup>>(() => defu({}, props.ui, appConfig.ui.avatarGroup))
|
||||
|
||||
const children = computed(() => {
|
||||
let children = slots.default?.()
|
||||
// @ts-ignore-next
|
||||
if (children.length && children[0].type.name === 'ContentSlot') {
|
||||
// @ts-ignore-next
|
||||
children = children[0].ctx.slots.default?.()
|
||||
}
|
||||
return children
|
||||
})
|
||||
|
||||
const max = computed(() => typeof props.max === 'string' ? parseInt(props.max, 10) : props.max)
|
||||
|
||||
const clones = computed(() => children.value.map((node, index) => {
|
||||
if (!props.max || (max.value && index < max.value)) {
|
||||
if (props.size) {
|
||||
node.props.size = props.size
|
||||
}
|
||||
|
||||
node.props.class = node.props.class || ''
|
||||
node.props.class += ` ${classNames(
|
||||
ui.value.ring,
|
||||
ui.value.margin
|
||||
)}`
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
if (max.value !== undefined && index === max.value) {
|
||||
return h(Avatar, {
|
||||
size: props.size,
|
||||
text: `+${children.value.length - max.value}`,
|
||||
class: classNames(
|
||||
ui.value.ring,
|
||||
ui.value.margin
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return null
|
||||
}).filter(Boolean).reverse())
|
||||
|
||||
return () => h('div', { class: ui.value.wrapper }, clones.value)
|
||||
}
|
||||
})
|
||||
@@ -1,79 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-row-reverse">
|
||||
<Avatar
|
||||
v-if="remainingGroupSize > 0"
|
||||
:size="size"
|
||||
:text="`+${remainingGroupSize}`"
|
||||
:class="avatarClass"
|
||||
/>
|
||||
<Avatar
|
||||
v-for="(avatar, index) of displayedGroup"
|
||||
:key="index"
|
||||
v-bind="avatar"
|
||||
:size="size"
|
||||
:class="avatarClass"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { classNames } from '../../utils'
|
||||
import Avatar from './Avatar.vue'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
const props = defineProps({
|
||||
group: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.avatar.size).includes(value)
|
||||
}
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
ringClass: {
|
||||
type: String,
|
||||
default: () => $ui.avatarGroup.ring
|
||||
},
|
||||
marginClass: {
|
||||
type: String,
|
||||
default: () => $ui.avatarGroup.margin
|
||||
}
|
||||
})
|
||||
|
||||
const avatars = computed(() => {
|
||||
return props.group.map((avatar) => {
|
||||
return typeof avatar === 'string' ? { src: avatar } : avatar
|
||||
})
|
||||
})
|
||||
|
||||
const displayedGroup = computed(() => {
|
||||
if (!props.max) { return [...avatars.value].reverse() }
|
||||
|
||||
return avatars.value.slice(0, props.max).reverse()
|
||||
})
|
||||
|
||||
const remainingGroupSize = computed(() => {
|
||||
if (!props.max) { return 0 }
|
||||
|
||||
return avatars.value.length - props.max
|
||||
})
|
||||
|
||||
const avatarClass = computed(() => {
|
||||
return classNames(
|
||||
props.ringClass,
|
||||
props.marginClass
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UAvatarGroup' }
|
||||
</script>
|
||||
@@ -4,50 +4,69 @@
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
const props = defineProps({
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.badge.size).includes(value)
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
size: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.badge.default.size,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.badge.size).includes(value)
|
||||
}
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.badge.default.color,
|
||||
validator (value: string) {
|
||||
return appConfig.ui.colors.includes(value)
|
||||
}
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.badge.default.variant,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.badge.variant).includes(value)
|
||||
}
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.badge>>,
|
||||
default: () => appConfig.ui.badge
|
||||
}
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.badge.variant).includes(value)
|
||||
setup (props) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.badge>>(() => defu({}, props.ui, appConfig.ui.badge))
|
||||
|
||||
const badgeClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.base,
|
||||
ui.value.font,
|
||||
ui.value.rounded,
|
||||
ui.value.size[props.size],
|
||||
ui.value.variant[props.variant]?.replaceAll('{color}', props.color)
|
||||
)
|
||||
})
|
||||
|
||||
return {
|
||||
badgeClass
|
||||
}
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.badge.base
|
||||
},
|
||||
rounded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const badgeClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
$ui.badge.size[props.size],
|
||||
$ui.badge.variant[props.variant],
|
||||
props.rounded ? 'rounded-full' : 'rounded-md'
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UBadge' }
|
||||
</script>
|
||||
|
||||
@@ -8,220 +8,224 @@
|
||||
>
|
||||
<Icon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="leadingIconClass" aria-hidden="true" />
|
||||
<slot>
|
||||
<span :class="[truncate ? 'text-left break-all line-clamp-1' : '', compact ? 'hidden sm:block' : '']">
|
||||
<span :class="[labelCompact && 'hidden sm:block']">{{ label }}</span>
|
||||
<span v-if="labelCompact" class="sm:hidden">{{ labelCompact }}</span>
|
||||
<span v-if="label" :class="[truncate ? 'text-left break-all line-clamp-1' : '']">
|
||||
{{ label }}
|
||||
</span>
|
||||
</slot>
|
||||
<Icon v-if="isTrailing && trailingIconName" :name="trailingIconName" :class="trailingIconClass" aria-hidden="true" />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, useSlots } from 'vue'
|
||||
<script lang="ts">
|
||||
import { ref, computed, defineComponent, useSlots } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import type { RouteLocationNormalized, RouteLocationRaw } from 'vue-router'
|
||||
import NuxtLink from '#app/components/nuxt-link'
|
||||
import { defu } from 'defu'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
import { NuxtLink } from '#components'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'button'
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Icon
|
||||
},
|
||||
block: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
labelCompact: {
|
||||
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)
|
||||
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
|
||||
},
|
||||
padded: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.button.default.size,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.button.size).includes(value)
|
||||
}
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.button.default.color,
|
||||
validator (value: string) {
|
||||
return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.button.color)].includes(value)
|
||||
}
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.button.default.variant,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.button.variant).includes(value)
|
||||
}
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
loadingIcon: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.button.default.loadingIcon
|
||||
},
|
||||
leadingIcon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
trailingIcon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
trailing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
leading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
to: {
|
||||
type: [String, Object] as PropType<string | RouteLocationNormalized | RouteLocationRaw>,
|
||||
default: null
|
||||
},
|
||||
target: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
ariaLabel: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
square: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
truncate: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.button>>,
|
||||
default: () => appConfig.ui.button
|
||||
}
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.button.variant).includes(value)
|
||||
setup (props) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.button>>(() => defu({}, props.ui, appConfig.ui.button))
|
||||
|
||||
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) || props.leadingIcon
|
||||
})
|
||||
|
||||
const isTrailing = computed(() => {
|
||||
return (props.icon && props.trailing) || (props.loading && props.trailing) || props.trailingIcon
|
||||
})
|
||||
|
||||
const isSquare = computed(() => props.square || (!slots.default && !props.label))
|
||||
|
||||
const buttonClass = computed(() => {
|
||||
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
|
||||
|
||||
return classNames(
|
||||
ui.value.base,
|
||||
ui.value.font,
|
||||
ui.value.rounded,
|
||||
ui.value.size[props.size],
|
||||
ui.value.gap[props.size],
|
||||
props.padded && ui.value[isSquare.value ? 'square' : 'spacing'][props.size],
|
||||
variant?.replaceAll('{color}', props.color),
|
||||
props.block ? 'w-full flex justify-center items-center' : 'inline-flex items-center'
|
||||
)
|
||||
})
|
||||
|
||||
const leadingIconName = computed(() => {
|
||||
if (props.loading) {
|
||||
return props.loadingIcon
|
||||
}
|
||||
|
||||
return props.leadingIcon || props.icon
|
||||
})
|
||||
|
||||
const trailingIconName = computed(() => {
|
||||
if (props.loading && !isLeading.value) {
|
||||
return props.loadingIcon
|
||||
}
|
||||
|
||||
return props.trailingIcon || props.icon
|
||||
})
|
||||
|
||||
const leadingIconClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.icon.base,
|
||||
ui.value.icon.size[props.size],
|
||||
props.loading && 'animate-spin'
|
||||
)
|
||||
})
|
||||
|
||||
const trailingIconClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.icon.base,
|
||||
ui.value.icon.size[props.size],
|
||||
props.loading && !isLeading.value && 'animate-spin'
|
||||
)
|
||||
})
|
||||
|
||||
return {
|
||||
button,
|
||||
buttonIs,
|
||||
buttonProps,
|
||||
isLeading,
|
||||
isTrailing,
|
||||
isSquare,
|
||||
buttonClass,
|
||||
leadingIconName,
|
||||
trailingIconName,
|
||||
leadingIconClass,
|
||||
trailingIconClass
|
||||
}
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
leadingIcon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
trailingIcon: {
|
||||
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] as PropType<string | RouteLocationNormalized | RouteLocationRaw>,
|
||||
default: null
|
||||
},
|
||||
target: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
ariaLabel: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
rounded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
roundedClass: {
|
||||
type: String,
|
||||
default: () => $ui.button.rounded
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.button.base
|
||||
},
|
||||
iconBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.button.icon.base
|
||||
},
|
||||
leadingIconClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
trailingIconClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
square: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
truncate: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
compact: {
|
||||
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) || props.leadingIcon
|
||||
})
|
||||
|
||||
const isTrailing = computed(() => {
|
||||
return (props.icon && props.trailing) || (props.loading && props.trailing) || props.trailingIcon
|
||||
})
|
||||
|
||||
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' : (props.compact ? 'compact' : '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' : props.roundedClass,
|
||||
props.customClass
|
||||
)
|
||||
})
|
||||
|
||||
const leadingIconName = computed(() => {
|
||||
if (props.loading) {
|
||||
return props.loadingIcon
|
||||
}
|
||||
|
||||
return props.leadingIcon || props.icon
|
||||
})
|
||||
|
||||
const trailingIconName = computed(() => {
|
||||
if (props.loading && !isLeading.value) {
|
||||
return props.loadingIcon
|
||||
}
|
||||
|
||||
return props.trailingIcon || props.icon
|
||||
})
|
||||
|
||||
const leadingIconClass = computed(() => {
|
||||
return classNames(
|
||||
props.iconBaseClass,
|
||||
$ui.button.icon.size[props.size],
|
||||
(!!slots.default || !!props.label?.length) && $ui.button.icon.leading[props.compact ? 'compactSpacing' : 'spacing'][props.size],
|
||||
props.leadingIconClass,
|
||||
props.loading && 'animate-spin'
|
||||
)
|
||||
})
|
||||
|
||||
const trailingIconClass = computed(() => {
|
||||
return classNames(
|
||||
props.iconBaseClass,
|
||||
$ui.button.icon.size[props.size],
|
||||
(!!slots.default || !!props.label?.length) && $ui.button.icon.trailing[props.compact ? 'compactSpacing' : 'spacing'][props.size],
|
||||
props.trailingIconClass,
|
||||
props.loading && !isLeading.value && 'animate-spin'
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UButton' }
|
||||
</script>
|
||||
|
||||
80
src/runtime/components/elements/ButtonGroup.ts
Normal file
80
src/runtime/components/elements/ButtonGroup.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { h, computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
size: {
|
||||
type: String,
|
||||
default: null,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.avatar.size).includes(value)
|
||||
}
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.buttonGroup>>,
|
||||
default: () => appConfig.ui.buttonGroup
|
||||
}
|
||||
},
|
||||
setup (props, { slots }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.buttonGroup>>(() => defu({}, props.ui, appConfig.ui.buttonGroup))
|
||||
|
||||
const children = computed(() => {
|
||||
let children = slots.default?.()
|
||||
// @ts-ignore-next
|
||||
if (children.length && children[0].type.name === 'ContentSlot') {
|
||||
// @ts-ignore-next
|
||||
children = children[0].ctx.slots.default?.()
|
||||
}
|
||||
return children
|
||||
})
|
||||
|
||||
const rounded = computed(() => ({
|
||||
'rounded-none': { left: 'rounded-l-none', right: 'rounded-r-none' },
|
||||
'rounded-sm': { left: 'rounded-l-sm', right: 'rounded-r-sm' },
|
||||
rounded: { left: 'rounded-l', right: 'rounded-r' },
|
||||
'rounded-md': { left: 'rounded-l-md', right: 'rounded-r-md' },
|
||||
'rounded-lg': { left: 'rounded-l-lg', right: 'rounded-r-lg' },
|
||||
'rounded-xl': { left: 'rounded-l-xl', right: 'rounded-r-xl' },
|
||||
'rounded-2xl': { left: 'rounded-l-2xl', right: 'rounded-r-2xl' },
|
||||
'rounded-3xl': { left: 'rounded-l-3xl', right: 'rounded-r-3xl' },
|
||||
'rounded-full': { left: 'rounded-l-full', right: 'rounded-r-full' }
|
||||
}[ui.value.rounded]))
|
||||
|
||||
const clones = computed(() => children.value.map((node, index) => {
|
||||
if (props.size) {
|
||||
node.props.size = props.size
|
||||
}
|
||||
|
||||
node.props.class = node.props.class || ''
|
||||
node.props.class += ' !shadow-none'
|
||||
node.props.ui = node.props.ui || {}
|
||||
node.props.ui.rounded = ''
|
||||
|
||||
if (index === 0) {
|
||||
node.props.ui.rounded = rounded.value.left
|
||||
}
|
||||
|
||||
if (index > 0) {
|
||||
node.props.class += ' -ml-px'
|
||||
}
|
||||
|
||||
if (index === children.value.length - 1) {
|
||||
node.props.ui.rounded = rounded.value.right
|
||||
}
|
||||
|
||||
return node
|
||||
}))
|
||||
|
||||
return () => h('div', { class: [ui.value.wrapper, ui.value.rounded, ui.value.shadow] }, clones.value)
|
||||
}
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Menu v-slot="{ open }" as="div" :class="wrapperClass" @mouseleave="onMouseLeave">
|
||||
<Menu v-slot="{ open }" as="div" :class="ui.wrapper" @mouseleave="onMouseLeave">
|
||||
<MenuButton
|
||||
ref="trigger"
|
||||
as="div"
|
||||
@@ -8,17 +8,17 @@
|
||||
role="button"
|
||||
@mouseover="onMouseOver"
|
||||
>
|
||||
<slot :open="open">
|
||||
<slot :open="open" :disabled="disabled">
|
||||
<button :disabled="disabled">
|
||||
Open
|
||||
</button>
|
||||
</slot>
|
||||
</MenuButton>
|
||||
|
||||
<div v-if="open && items.length" ref="container" :class="[containerClass, widthClass]" @mouseover="onMouseOver">
|
||||
<transition appear v-bind="transitionClass">
|
||||
<MenuItems :class="[baseClass, divideClass, ringClass, roundedClass, shadowClass, backgroundClass]" static>
|
||||
<div v-for="(subItems, index) of items" :key="index" :class="groupClass">
|
||||
<div v-if="open && items.length" ref="container" :class="[ui.container, ui.width]" @mouseover="onMouseOver">
|
||||
<transition appear v-bind="ui.transition">
|
||||
<MenuItems :class="[ui.base, ui.divide, ui.ring, ui.rounded, ui.shadow, ui.background]" static>
|
||||
<div v-for="(subItems, index) of items" :key="index" :class="ui.spacing">
|
||||
<MenuItem v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ active, disabled: itemDisabled }" :disabled="item.disabled">
|
||||
<Component
|
||||
v-bind="omit(item, ['click'])"
|
||||
@@ -26,13 +26,13 @@
|
||||
:class="resolveItemClass({ active, disabled: itemDisabled })"
|
||||
@click="item.click"
|
||||
>
|
||||
<slot :name="item.slot" :item="item">
|
||||
<Icon v-if="item.icon" :name="item.icon" :class="[itemIconClass, item.iconClass]" />
|
||||
<Avatar v-if="item.avatar" v-bind="{ size: 'xxs', ...item.avatar }" :class="itemAvatarClass" />
|
||||
<slot :name="item.slot || 'item'" :item="item">
|
||||
<Icon v-if="item.icon" :name="item.icon" :class="[ui.item.icon.base, active ? ui.item.icon.active : ui.item.icon.inactive, item.iconClass]" />
|
||||
<Avatar v-else-if="item.avatar" v-bind="{ size: ui.item.avatar.size, ...item.avatar }" :class="ui.item.avatar.base" />
|
||||
|
||||
<span class="truncate">{{ item.label }}</span>
|
||||
|
||||
<span v-if="item.shortcuts?.length" :class="itemShortcutsClass">
|
||||
<span v-if="item.shortcuts?.length" :class="ui.item.shortcuts">
|
||||
<kbd v-for="shortcut of item.shortcuts" :key="shortcut" class="font-sans">{{ shortcut }}</kbd>
|
||||
</span>
|
||||
</slot>
|
||||
@@ -45,29 +45,39 @@
|
||||
</Menu>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItems,
|
||||
MenuItem
|
||||
} from '@headlessui/vue'
|
||||
<script lang="ts">
|
||||
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
|
||||
import type { PropType } from 'vue'
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { defineComponent, ref, computed, onMounted } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import NuxtLink from '#app/components/nuxt-link'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import Avatar from '../elements/Avatar.vue'
|
||||
import { classNames, omit } from '../../utils'
|
||||
import { usePopper } from '../../composables/usePopper'
|
||||
import type { Avatar as AvatarType } from '../../types/avatar'
|
||||
import type { PopperOptions } from '../../types'
|
||||
import $ui from '#build/ui'
|
||||
import { NuxtLink } from '#components'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array as PropType<{
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
// eslint-disable-next-line vue/no-reserved-component-names
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItems,
|
||||
MenuItem,
|
||||
Icon,
|
||||
Avatar
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array as PropType<{
|
||||
to?: RouteLocationNormalized
|
||||
exact?: boolean
|
||||
label: string
|
||||
@@ -79,176 +89,123 @@ const props = defineProps({
|
||||
click?: Function
|
||||
shortcuts?: string[]
|
||||
}[][]>,
|
||||
default: () => []
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'click',
|
||||
validator: (value: string) => {
|
||||
return ['click', 'hover'].includes(value)
|
||||
default: () => []
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'click',
|
||||
validator: (value: string) => {
|
||||
return ['click', 'hover'].includes(value)
|
||||
}
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
popper: {
|
||||
type: Object as PropType<PopperOptions>,
|
||||
default: () => ({})
|
||||
},
|
||||
openDelay: {
|
||||
type: Number,
|
||||
default: 50
|
||||
},
|
||||
closeDelay: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.dropdown>>,
|
||||
default: () => appConfig.ui.dropdown
|
||||
}
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.wrapper
|
||||
},
|
||||
containerClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.container
|
||||
},
|
||||
widthClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.width
|
||||
},
|
||||
backgroundClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.background
|
||||
},
|
||||
shadowClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.shadow
|
||||
},
|
||||
roundedClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.rounded
|
||||
},
|
||||
ringClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.ring
|
||||
},
|
||||
divideClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.divide
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.base
|
||||
},
|
||||
transitionClass: {
|
||||
type: Object,
|
||||
default: () => $ui.dropdown.transition
|
||||
},
|
||||
groupClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.group
|
||||
},
|
||||
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
|
||||
},
|
||||
itemShortcutsClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.item.shortcuts
|
||||
},
|
||||
popperOptions: {
|
||||
type: Object as PropType<PopperOptions>,
|
||||
default: () => ({})
|
||||
},
|
||||
openDelay: {
|
||||
type: Number,
|
||||
default: 50
|
||||
},
|
||||
closeDelay: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
setup (props) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const popperOptions = computed<PopperOptions>(() => defu({}, props.popperOptions, $ui.dropdown.popperOptions))
|
||||
const ui = computed<Partial<typeof appConfig.ui.dropdown>>(() => defu({}, props.ui, appConfig.ui.dropdown))
|
||||
|
||||
const [trigger, container] = usePopper(popperOptions.value)
|
||||
const popper = computed<PopperOptions>(() => defu({}, props.popper, ui.value.popper as PopperOptions))
|
||||
|
||||
function resolveItemClass ({ active, disabled }: { active: boolean, disabled: boolean }) {
|
||||
return classNames(
|
||||
props.itemBaseClass,
|
||||
active ? props.itemActiveClass : props.itemInactiveClass,
|
||||
disabled && props.itemDisabledClass
|
||||
)
|
||||
}
|
||||
const [trigger, container] = usePopper(popper.value)
|
||||
|
||||
// https://github.com/tailwindlabs/headlessui/blob/f66f4926c489fc15289d528294c23a3dc2aee7b1/packages/%40headlessui-vue/src/components/menu/menu.ts#L131
|
||||
const menuApi = ref<any>(null)
|
||||
|
||||
let openTimeout: NodeJS.Timeout | null = null
|
||||
let closeTimeout: NodeJS.Timeout | null = null
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
// @ts-expect-error internals
|
||||
const menuProvides = trigger.value?.$.provides
|
||||
if (!menuProvides) {
|
||||
return
|
||||
function resolveItemClass ({ active, disabled }: { active: boolean, disabled: boolean }) {
|
||||
return classNames(
|
||||
ui.value.item.base,
|
||||
active ? ui.value.item.active : ui.value.item.inactive,
|
||||
disabled && ui.value.item.disabled
|
||||
)
|
||||
}
|
||||
const menuProvidesSymbols = Object.getOwnPropertySymbols(menuProvides)
|
||||
menuApi.value = menuProvidesSymbols.length && menuProvides[menuProvidesSymbols[0]]
|
||||
}, 200)
|
||||
|
||||
// https://github.com/tailwindlabs/headlessui/blob/f66f4926c489fc15289d528294c23a3dc2aee7b1/packages/%40headlessui-vue/src/components/menu/menu.ts#L131
|
||||
const menuApi = ref<any>(null)
|
||||
|
||||
let openTimeout: NodeJS.Timeout | null = null
|
||||
let closeTimeout: NodeJS.Timeout | null = null
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
// @ts-expect-error internals
|
||||
const menuProvides = trigger.value?.$.provides
|
||||
if (!menuProvides) {
|
||||
return
|
||||
}
|
||||
const menuProvidesSymbols = Object.getOwnPropertySymbols(menuProvides)
|
||||
menuApi.value = menuProvidesSymbols.length && menuProvides[menuProvidesSymbols[0]]
|
||||
}, 200)
|
||||
})
|
||||
|
||||
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
|
||||
}, props.openDelay)
|
||||
}
|
||||
|
||||
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
|
||||
}, props.closeDelay)
|
||||
}
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
trigger,
|
||||
container,
|
||||
resolveItemClass,
|
||||
onMouseOver,
|
||||
onMouseLeave,
|
||||
omit,
|
||||
NuxtLink
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
}, props.openDelay)
|
||||
}
|
||||
|
||||
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
|
||||
}, props.closeDelay)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UDropdown' }
|
||||
</script>
|
||||
|
||||
@@ -10,7 +10,3 @@ defineProps({
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UIcon' }
|
||||
</script>
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
<template>
|
||||
<button v-if="isButton" v-bind="$attrs" :class="inactiveClass">
|
||||
<slot />
|
||||
</button>
|
||||
<a v-else-if="isExternalLink" v-bind="$attrs" :href="to" :target="target" :class="inactiveClass">
|
||||
<slot />
|
||||
</a>
|
||||
<router-link
|
||||
v-else
|
||||
v-slot="{ href, navigate, isActive, isExactActive }"
|
||||
v-bind="$props as RouterLinkProps"
|
||||
custom
|
||||
>
|
||||
<a
|
||||
v-bind="$attrs"
|
||||
:href="href"
|
||||
:class="resolveLinkClass({ isActive, isExactActive })"
|
||||
@click="navigate"
|
||||
>
|
||||
<slot v-bind="{ isActive: exact ? isExactActive : isActive }" />
|
||||
</a>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import type { RouteLocationNormalized, RouterLinkProps } from 'vue-router'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
const props = defineProps({
|
||||
// @ts-expect-error internal props
|
||||
...RouterLink.props,
|
||||
to: {
|
||||
type: [String, Object] as PropType<string | RouteLocationNormalized>,
|
||||
default: null
|
||||
},
|
||||
inactiveClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
exact: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
target: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const isExternalLink = computed(() => {
|
||||
return typeof props.to === 'string' && props.to.startsWith('http')
|
||||
})
|
||||
const isButton = computed(() => {
|
||||
return !props.to
|
||||
})
|
||||
|
||||
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: 'ULink',
|
||||
inheritAttrs: false
|
||||
}
|
||||
</script>
|
||||
36
src/runtime/components/elements/LinkCustom.vue
Normal file
36
src/runtime/components/elements/LinkCustom.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<NuxtLink
|
||||
v-slot="{ href, navigate, exact, isActive, isExactActive }"
|
||||
custom
|
||||
>
|
||||
<a
|
||||
v-bind="$attrs"
|
||||
:href="href"
|
||||
:class="resolveLinkClass({ isActive, isExactActive })"
|
||||
@click="navigate"
|
||||
>
|
||||
<slot v-bind="{ isActive: exact ? isExactActive : isActive }" />
|
||||
</a>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
activeClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
inactiveClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
function resolveLinkClass ({ isActive, isExactActive }: { isActive: boolean, isExactActive: boolean }) {
|
||||
if (isActive || isExactActive) {
|
||||
return props.activeClass
|
||||
}
|
||||
|
||||
return props.inactiveClass
|
||||
}
|
||||
</script>
|
||||
@@ -1,106 +0,0 @@
|
||||
<template>
|
||||
<div class="rounded-md p-4" :class="variantClass">
|
||||
<div class="flex">
|
||||
<div class="inline-flex flex-shrink-0">
|
||||
<Icon v-if="iconName" :name="iconName" :class="iconClass" class="h-5 w-5" />
|
||||
</div>
|
||||
<div class="ml-3 flex-1 md:flex md:justify-between">
|
||||
<p v-if="title" class="text-sm leading-5" :class="titleClass">
|
||||
{{ title }}
|
||||
</p>
|
||||
<p v-if="link" class="mt-3 text-sm leading-5 md:mt-0 md:ml-6">
|
||||
<NuxtLink
|
||||
:to="to"
|
||||
class="whitespace-nowrap font-medium"
|
||||
:class="linkClass"
|
||||
@click="click && click()"
|
||||
>
|
||||
{{ link }} →
|
||||
</NuxtLink>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
|
||||
const props = defineProps({
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'info',
|
||||
validator (value: string) {
|
||||
return ['info', 'warning', 'error', 'success'].includes(value)
|
||||
}
|
||||
},
|
||||
to: {
|
||||
type: [String, Object] as PropType<string | RouteLocationNormalized>,
|
||||
default: null
|
||||
},
|
||||
click: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
link: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const iconName = computed(() => {
|
||||
return ({
|
||||
info: 'i-heroicons-information-circle',
|
||||
warning: 'i-heroicons-exclamation',
|
||||
error: 'i-heroicons-x-circle',
|
||||
success: 'i-heroicons-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 lang="ts">
|
||||
export default { name: 'UAlert' }
|
||||
</script>
|
||||
@@ -1,108 +0,0 @@
|
||||
<template>
|
||||
<Modal v-model="isOpen" :appear="appear" class="w-full" @close="onClose">
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div v-if="icon" :class="iconWrapperClass" class="mx-auto flex-shrink-0 flex items-center justify-center rounded-full sm:mx-0">
|
||||
<Icon :name="icon" :class="iconClass" />
|
||||
</div>
|
||||
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left space-y-2">
|
||||
<DialogTitle v-if="title" as="h3" :class="titleClass">
|
||||
{{ title }}
|
||||
</DialogTitle>
|
||||
<DialogDescription v-if="description" as="p" :class="descriptionClass">
|
||||
{{ description }}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 space-y-3 sm:space-y-0 sm:mt-4 sm:flex sm:gap-3" :class="{ 'sm:flex-row-reverse': !leadingButtons }">
|
||||
<Button variant="primary" :label="confirmLabel" block custom-class="sm:w-auto" @click="onConfirm" />
|
||||
<Button variant="secondary" :label="cancelLabel" block custom-class="sm:w-auto" @click="onCancel" />
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { DialogTitle, DialogDescription } from '@headlessui/vue'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import Modal from '../overlays/Modal.vue'
|
||||
import Button from '../elements/Button.vue'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
appear: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
iconClass: {
|
||||
type: String,
|
||||
default: () => $ui.alertDialog.icon.base
|
||||
},
|
||||
iconWrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.alertDialog.icon.wrapper
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
titleClass: {
|
||||
type: String,
|
||||
default: () => $ui.alertDialog.title
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
descriptionClass: {
|
||||
type: String,
|
||||
default: () => $ui.alertDialog.description
|
||||
},
|
||||
confirmLabel: {
|
||||
type: String,
|
||||
default: 'Confirm'
|
||||
},
|
||||
cancelLabel: {
|
||||
type: String,
|
||||
default: 'Cancel'
|
||||
},
|
||||
leadingButtons: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'confirm', 'cancel', 'close'])
|
||||
|
||||
const isOpen = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
function onConfirm () {
|
||||
emit('confirm')
|
||||
isOpen.value = false
|
||||
}
|
||||
function onCancel () {
|
||||
emit('cancel')
|
||||
isOpen.value = false
|
||||
}
|
||||
function onClose () {
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UAlertDialog' }
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<div :class="ui.wrapper">
|
||||
<div class="flex items-center h-5">
|
||||
<input
|
||||
:id="name"
|
||||
@@ -9,102 +9,90 @@
|
||||
:value="value"
|
||||
:disabled="disabled"
|
||||
type="checkbox"
|
||||
:class="inputClass"
|
||||
:class="[ui.base, ui.custom]"
|
||||
@focus="$emit('focus', $event)"
|
||||
@blur="$emit('blur', $event)"
|
||||
>
|
||||
</div>
|
||||
<div v-if="label || $slots.label" class="ml-3 text-sm">
|
||||
<label :for="name" :class="labelClass">
|
||||
<label :for="name" :class="ui.label">
|
||||
<slot name="label">{{ label }}</slot>
|
||||
<span v-if="required" :class="requiredClass">*</span>
|
||||
<span v-if="required" :class="ui.required">*</span>
|
||||
</label>
|
||||
<p v-if="help" :class="helpClass">
|
||||
<p v-if="help" :class="ui.help">
|
||||
{{ help }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: [String, Number, Boolean],
|
||||
default: null
|
||||
},
|
||||
modelValue: {
|
||||
type: [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>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UCheckbox' }
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Number, Boolean],
|
||||
default: null
|
||||
},
|
||||
modelValue: {
|
||||
type: [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
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.checkbox>>,
|
||||
default: () => appConfig.ui.checkbox
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'focus', 'blur'],
|
||||
setup (props, { emit }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.checkbox>>(() => defu({}, props.ui, appConfig.ui.checkbox))
|
||||
|
||||
const isChecked = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
isChecked
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<div v-if="label || $slots.label" :class="labelWrapperClass">
|
||||
<label :for="name" :class="labelClass">
|
||||
<slot name="label">{{ label }}</slot>
|
||||
<span v-if="required" :class="requiredClass">*</span>
|
||||
</label>
|
||||
<span v-if="$slots.hint || hint" :class="hintClass">
|
||||
<slot name="hint">{{ hint }}</slot>
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="description" :class="descriptionClass">
|
||||
{{ description }}
|
||||
</p>
|
||||
<div :class="!!label && containerClass">
|
||||
<slot />
|
||||
<p v-if="help" :class="helpClass">
|
||||
{{ help }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import $ui from '#build/ui'
|
||||
|
||||
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>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UFormGroup' }
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<div :class="ui.wrapper">
|
||||
<input
|
||||
:id="name"
|
||||
ref="input"
|
||||
@@ -8,7 +8,7 @@
|
||||
:type="type"
|
||||
:required="required"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:disabled="disabled || loading"
|
||||
:readonly="readonly"
|
||||
:autocomplete="autocomplete"
|
||||
:spellcheck="spellcheck"
|
||||
@@ -18,176 +18,216 @@
|
||||
@blur="$emit('blur', $event)"
|
||||
>
|
||||
<slot />
|
||||
<div v-if="isLeading" :class="iconLeadingWrapperClass">
|
||||
<Icon v-if="iconName" :name="iconName" :class="iconClass" />
|
||||
<div v-if="isLeading && leadingIconName" :class="leadingIconClass">
|
||||
<Icon :name="leadingIconName" :class="iconClass" />
|
||||
</div>
|
||||
<div v-if="isTrailing" :class="iconTrailingWrapperClass">
|
||||
<Icon v-if="iconName" :name="iconName" :class="iconClass" />
|
||||
<div v-if="isTrailing && trailingIconName" :class="trailingIconClass">
|
||||
<Icon :name="trailingIconName" :class="iconClass" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
<script lang="ts">
|
||||
import { ref, computed, onMounted, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Icon
|
||||
},
|
||||
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: Boolean,
|
||||
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)
|
||||
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: Boolean,
|
||||
default: null
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
loadingIcon: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.input.default.loadingIcon
|
||||
},
|
||||
leadingIcon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
trailingIcon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
trailing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
leading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.input.default.size,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.input.size).includes(value)
|
||||
}
|
||||
},
|
||||
appearance: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.input.default.appearance,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.input.appearance).includes(value)
|
||||
}
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.input>>,
|
||||
default: () => appConfig.ui.input
|
||||
}
|
||||
},
|
||||
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)
|
||||
emits: ['update:modelValue', 'focus', 'blur'],
|
||||
setup (props, { emit }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.input>>(() => defu({}, props.ui, appConfig.ui.input))
|
||||
|
||||
const input = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const autoFocus = () => {
|
||||
if (props.autofocus) {
|
||||
input.value?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const onInput = (value: string) => {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
autoFocus()
|
||||
}, 100)
|
||||
})
|
||||
|
||||
const inputClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.base,
|
||||
ui.value.size[props.size],
|
||||
ui.value.spacing[props.size],
|
||||
ui.value.appearance[props.appearance],
|
||||
isLeading.value && ui.value.leading.spacing[props.size],
|
||||
isTrailing.value && ui.value.trailing.spacing[props.size],
|
||||
ui.value.custom
|
||||
)
|
||||
})
|
||||
|
||||
const isLeading = computed(() => {
|
||||
return (props.icon && props.leading) || (props.icon && !props.trailing) || (props.loading && !props.trailing) || props.leadingIcon
|
||||
})
|
||||
|
||||
const isTrailing = computed(() => {
|
||||
return (props.icon && props.trailing) || (props.loading && props.trailing) || props.trailingIcon
|
||||
})
|
||||
|
||||
const leadingIconName = computed(() => {
|
||||
if (props.loading) {
|
||||
return props.loadingIcon
|
||||
}
|
||||
|
||||
return props.leadingIcon || props.icon
|
||||
})
|
||||
|
||||
const trailingIconName = computed(() => {
|
||||
if (props.loading && !isLeading.value) {
|
||||
return props.loadingIcon
|
||||
}
|
||||
|
||||
return props.trailingIcon || props.icon
|
||||
})
|
||||
|
||||
const iconClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.icon.base,
|
||||
ui.value.icon.size[props.size],
|
||||
props.loading && 'animate-spin'
|
||||
)
|
||||
})
|
||||
|
||||
const leadingIconClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.icon.leading.wrapper,
|
||||
ui.value.icon.leading.spacing[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
const trailingIconClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.icon.trailing.wrapper,
|
||||
ui.value.icon.trailing.spacing[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
isLeading,
|
||||
isTrailing,
|
||||
inputClass,
|
||||
iconClass,
|
||||
leadingIconName,
|
||||
leadingIconClass,
|
||||
trailingIconName,
|
||||
trailingIconClass,
|
||||
onInput
|
||||
}
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'focus', 'blur'])
|
||||
|
||||
const input = ref<HTMLInputElement | null>(null)
|
||||
|
||||
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>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UInput' }
|
||||
</script>
|
||||
|
||||
78
src/runtime/components/forms/InputGroup.vue
Normal file
78
src/runtime/components/forms/InputGroup.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div :class="ui.wrapper">
|
||||
<div v-if="label || $slots.label" :class="ui.labelWrapper">
|
||||
<label :for="name" :class="ui.label">
|
||||
<slot name="label">{{ label }}</slot>
|
||||
<span v-if="required" :class="ui.required">*</span>
|
||||
</label>
|
||||
<span v-if="$slots.hint || hint" :class="ui.hint">
|
||||
<slot name="hint">{{ hint }}</slot>
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="description" :class="ui.description">
|
||||
{{ description }}
|
||||
</p>
|
||||
<div :class="!!label && ui.container">
|
||||
<slot />
|
||||
<p v-if="help" :class="ui.help">
|
||||
{{ help }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
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
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.inputGroup>>,
|
||||
default: () => appConfig.ui.inputGroup
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.inputGroup>>(() => defu({}, props.ui, appConfig.ui.inputGroup))
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<div :class="ui.wrapper">
|
||||
<div class="flex items-center h-5">
|
||||
<input
|
||||
:id="`${name}-${value}`"
|
||||
@@ -9,102 +9,90 @@
|
||||
:value="value"
|
||||
:disabled="disabled"
|
||||
type="radio"
|
||||
:class="radioClass"
|
||||
:class="[ui.base, ui.custom]"
|
||||
@focus="$emit('focus', $event)"
|
||||
@blur="$emit('blur', $event)"
|
||||
>
|
||||
</div>
|
||||
<div v-if="label || $slots.label" class="ml-3 text-sm">
|
||||
<label :for="`${name}-${value}`" :class="labelClass">
|
||||
<label :for="`${name}-${value}`" :class="ui.label">
|
||||
<slot name="label">{{ label }}</slot>
|
||||
<span v-if="required" :class="requiredClass">*</span>
|
||||
<span v-if="required" :class="ui.required">*</span>
|
||||
</label>
|
||||
<p v-if="help" :class="helpClass">
|
||||
<p v-if="help" :class="ui.help">
|
||||
{{ help }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
const props = defineProps({
|
||||
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 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 lang="ts">
|
||||
export default { name: 'URadio' }
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
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
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.radio>>,
|
||||
default: () => appConfig.ui.radio
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'focus', 'blur'],
|
||||
setup (props, { emit }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.radio>>(() => defu({}, props.ui, appConfig.ui.radio))
|
||||
|
||||
const isChecked = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
isChecked
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<div :class="ui.wrapper">
|
||||
<select
|
||||
:id="name"
|
||||
:name="name"
|
||||
@@ -17,7 +17,7 @@
|
||||
:label="option[textAttribute]"
|
||||
>
|
||||
<option
|
||||
v-for="(childOption, index2) in option.children"
|
||||
v-for="(childOption, index2) in (option.children as any[])"
|
||||
:key="`${childOption[valueAttribute]}-${index}-${index2}`"
|
||||
:value="childOption[valueAttribute]"
|
||||
:selected="childOption[valueAttribute] === normalizedValue"
|
||||
@@ -36,169 +36,197 @@
|
||||
</template>
|
||||
</select>
|
||||
|
||||
<div v-if="icon" :class="iconWrapperClass">
|
||||
<div v-if="icon" :class="leadingIconClass">
|
||||
<Icon :name="icon" :class="iconClass" />
|
||||
</div>
|
||||
|
||||
<span :class="trailingIconClass">
|
||||
<Icon name="i-heroicons-chevron-down-20-solid" :class="iconClass" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { get } from 'lodash-es'
|
||||
import { classNames } from '../../utils'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
const props = defineProps({
|
||||
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: string) {
|
||||
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
|
||||
},
|
||||
appearance: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.select.appearance).includes(value)
|
||||
}
|
||||
},
|
||||
textAttribute: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
valueAttribute: {
|
||||
type: String,
|
||||
default: 'value'
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const onInput = (value: string) => {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
const guessOptionValue = (option: any) => {
|
||||
return get(option, props.valueAttribute, get(option, props.textAttribute))
|
||||
}
|
||||
|
||||
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 {
|
||||
[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 normalizeModelValue = normalizeOption(props.modelValue)
|
||||
const foundOption = normalizedOptionsWithPlaceholder.value.find(option => option[props.valueAttribute] === normalizeModelValue[props.valueAttribute])
|
||||
if (!foundOption) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return foundOption[props.valueAttribute]
|
||||
})
|
||||
|
||||
const selectClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
$ui.select.size[props.size],
|
||||
$ui.select.spacing[props.size],
|
||||
$ui.select.appearance[props.appearance],
|
||||
!!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 lang="ts">
|
||||
export default { name: 'USelect' }
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { get } from 'lodash-es'
|
||||
import { defu } from 'defu'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import { classNames } from '../../utils'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Icon
|
||||
},
|
||||
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
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.select.default.size,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.select.size).includes(value)
|
||||
}
|
||||
},
|
||||
appearance: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.select.default.appearance,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.select.appearance).includes(value)
|
||||
}
|
||||
},
|
||||
textAttribute: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
valueAttribute: {
|
||||
type: String,
|
||||
default: 'value'
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.select>>,
|
||||
default: () => appConfig.ui.select
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'focus', 'blur'],
|
||||
setup (props, { emit }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.select>>(() => defu({}, props.ui, appConfig.ui.select))
|
||||
|
||||
const onInput = (value: string) => {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
const guessOptionValue = (option: any) => {
|
||||
return get(option, props.valueAttribute, get(option, props.textAttribute))
|
||||
}
|
||||
|
||||
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 {
|
||||
[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 normalizeModelValue = normalizeOption(props.modelValue)
|
||||
const foundOption = normalizedOptionsWithPlaceholder.value.find(option => option[props.valueAttribute] === normalizeModelValue[props.valueAttribute])
|
||||
if (!foundOption) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return foundOption[props.valueAttribute]
|
||||
})
|
||||
|
||||
const selectClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.base,
|
||||
ui.value.size[props.size],
|
||||
ui.value.spacing[props.size],
|
||||
ui.value.appearance[props.appearance],
|
||||
!!props.icon && ui.value.leading.spacing[props.size],
|
||||
ui.value.trailing.spacing[props.size],
|
||||
ui.value.custom
|
||||
)
|
||||
})
|
||||
|
||||
const iconClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.icon.base,
|
||||
ui.value.icon.size[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
const leadingIconClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.icon.leading.wrapper,
|
||||
ui.value.icon.leading.spacing[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
const trailingIconClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.icon.trailing.wrapper,
|
||||
ui.value.icon.trailing.spacing[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
normalizedOptionsWithPlaceholder,
|
||||
normalizedValue,
|
||||
selectClass,
|
||||
iconClass,
|
||||
leadingIconClass,
|
||||
trailingIconClass,
|
||||
onInput
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,346 +0,0 @@
|
||||
<template>
|
||||
<Combobox
|
||||
v-slot="{ open }"
|
||||
:by="by"
|
||||
:model-value="modelValue"
|
||||
:multiple="multiple"
|
||||
:nullable="nullable"
|
||||
:disabled="disabled"
|
||||
as="div"
|
||||
:class="wrapperClass"
|
||||
@update:model-value="onUpdate"
|
||||
>
|
||||
<input :value="modelValue" :required="required" class="absolute inset-0 w-px opacity-0 cursor-default" tabindex="-1" aria-hidden="true">
|
||||
|
||||
<ComboboxButton ref="trigger" v-slot="{ disabled: buttonDisabled }" as="div" role="button" class="inline-flex w-full">
|
||||
<slot :open="open" :disabled="buttonDisabled">
|
||||
<button :class="selectCustomClass" :disabled="disabled" type="button">
|
||||
<slot name="label">
|
||||
<span v-if="modelValue" class="block truncate">{{ (modelValue as any)[textAttribute] }}</span>
|
||||
<span v-else class="block truncate u-text-gray-400">{{ placeholder }}</span>
|
||||
</slot>
|
||||
<slot name="icon">
|
||||
<span :class="iconWrapperClass">
|
||||
<Icon v-if="icon" :name="icon" :class="iconClass" aria-hidden="true" />
|
||||
</span>
|
||||
</slot>
|
||||
</button>
|
||||
</slot>
|
||||
</ComboboxButton>
|
||||
|
||||
<div v-if="open" ref="container" :class="[listContainerClass, listWidthClass]">
|
||||
<transition appear v-bind="listTransitionClass">
|
||||
<ComboboxOptions static :class="listBaseClass">
|
||||
<ComboboxInput
|
||||
v-if="searchable"
|
||||
ref="searchInput"
|
||||
:display-value="() => query"
|
||||
name="q"
|
||||
placeholder="Search..."
|
||||
autofocus
|
||||
autocomplete="off"
|
||||
:class="listInputClass"
|
||||
@change="query = $event.target.value"
|
||||
/>
|
||||
<ComboboxOption
|
||||
v-for="(option, index) in filteredOptions"
|
||||
v-slot="{ active, selected, disabled: optionDisabled }"
|
||||
:key="index"
|
||||
as="template"
|
||||
:value="option"
|
||||
:disabled="option.disabled"
|
||||
>
|
||||
<li :class="resolveOptionClass({ active, selected, disabled: optionDisabled })">
|
||||
<div :class="listOptionContainerClass">
|
||||
<slot name="option" :option="option" :active="active" :selected="selected">
|
||||
<span class="block truncate">{{ option[textAttribute] }}</span>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<span v-if="selected" :class="resolveOptionIconClass({ active })">
|
||||
<Icon v-if="listOptionIcon" :name="listOptionIcon" :class="listOptionIconSizeClass" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ComboboxOption>
|
||||
|
||||
<ComboboxOption v-if="creatable && queryOption && !filteredOptions.length" v-slot="{ active, selected }" :value="queryOption" as="template">
|
||||
<li :class="resolveOptionClass({ active, selected })">
|
||||
<div :class="listOptionContainerClass">
|
||||
<slot name="option-create" :option="queryOption" :active="active" :selected="selected">
|
||||
<span class="block truncate">Create "{{ queryOption[textAttribute] }}"</span>
|
||||
</slot>
|
||||
</div>
|
||||
</li>
|
||||
</ComboboxOption>
|
||||
<p v-else-if="searchable && query && !filteredOptions.length" :class="listOptionEmptyClass">
|
||||
<slot name="option-empty" :query="query">
|
||||
No results found for "{{ query }}".
|
||||
</slot>
|
||||
</p>
|
||||
</ComboboxOptions>
|
||||
</transition>
|
||||
</div>
|
||||
</Combobox>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import type { PropType, ComponentPublicInstance } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxButton,
|
||||
ComboboxOptions,
|
||||
ComboboxOption,
|
||||
ComboboxInput
|
||||
} from '@headlessui/vue'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import { classNames } from '../../utils'
|
||||
import { usePopper } from '../../composables/usePopper'
|
||||
import type { PopperOptions } from '../../types'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number, Object, Array],
|
||||
default: ''
|
||||
},
|
||||
by: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
options: {
|
||||
type: Array as PropType<{ [key: string]: any, disabled?: boolean }[] | string[]>,
|
||||
default: () => []
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
nullable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
searchable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
creatable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'Select an option'
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.selectCustom.size).includes(value)
|
||||
}
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.wrapper
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.base
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.icon.name
|
||||
},
|
||||
iconBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.icon.base
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
appearance: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.selectCustom.appearance).includes(value)
|
||||
}
|
||||
},
|
||||
listBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.base
|
||||
},
|
||||
listContainerClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.container
|
||||
},
|
||||
listWidthClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.width
|
||||
},
|
||||
listInputClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.input
|
||||
},
|
||||
listTransitionClass: {
|
||||
type: Object,
|
||||
default: () => $ui.selectCustom.list.transition
|
||||
},
|
||||
listOptionBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.base
|
||||
},
|
||||
listOptionContainerClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.container
|
||||
},
|
||||
listOptionActiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.active
|
||||
},
|
||||
listOptionInactiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.inactive
|
||||
},
|
||||
listOptionSelectedClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.selected
|
||||
},
|
||||
listOptionUnselectedClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.unselected
|
||||
},
|
||||
listOptionDisabledClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.disabled
|
||||
},
|
||||
listOptionEmptyClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.empty
|
||||
},
|
||||
listOptionIcon: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.icon.name
|
||||
},
|
||||
listOptionIconBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.icon.base
|
||||
},
|
||||
listOptionIconActiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.icon.active
|
||||
},
|
||||
listOptionIconInactiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.icon.inactive
|
||||
},
|
||||
listOptionIconSizeClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.icon.size
|
||||
},
|
||||
textAttribute: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
searchAttributes: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
popperOptions: {
|
||||
type: Object as PropType<PopperOptions>,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'open', 'close'])
|
||||
|
||||
const popperOptions = computed<PopperOptions>(() => defu({}, props.popperOptions, $ui.selectCustom.popperOptions))
|
||||
|
||||
const [trigger, container] = usePopper(popperOptions.value)
|
||||
|
||||
const query = ref('')
|
||||
const searchInput = ref<ComponentPublicInstance<HTMLElement>>()
|
||||
|
||||
const selectCustomClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
$ui.selectCustom.size[props.size],
|
||||
$ui.selectCustom.spacing[props.size],
|
||||
$ui.selectCustom.appearance[props.appearance],
|
||||
$ui.selectCustom.trailing.spacing[props.size],
|
||||
props.customClass
|
||||
)
|
||||
})
|
||||
|
||||
const iconClass = computed(() => {
|
||||
return classNames(
|
||||
props.iconBaseClass,
|
||||
$ui.selectCustom.icon.size[props.size],
|
||||
'mr-2'
|
||||
)
|
||||
})
|
||||
|
||||
const filteredOptions = computed(() =>
|
||||
query.value === ''
|
||||
? props.options
|
||||
: (props.options as any[]).filter((option: any) => {
|
||||
return (props.searchAttributes?.length ? props.searchAttributes : [props.textAttribute]).some((searchAttribute: any) => {
|
||||
return typeof option === 'string' ? option.search(new RegExp(query.value, 'i')) !== -1 : (option[searchAttribute] && option[searchAttribute].search(new RegExp(query.value, 'i')) !== -1)
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
const queryOption = computed(() => {
|
||||
return query.value === '' ? null : { [props.textAttribute]: query.value }
|
||||
})
|
||||
|
||||
const iconWrapperClass = classNames(
|
||||
$ui.selectCustom.icon.trailing.wrapper
|
||||
)
|
||||
|
||||
watch(container, (value) => {
|
||||
if (value) {
|
||||
emit('open')
|
||||
} else {
|
||||
emit('close')
|
||||
}
|
||||
})
|
||||
|
||||
function resolveOptionClass ({ active, selected, disabled }: { active: boolean, selected: boolean, disabled?: boolean }) {
|
||||
return classNames(
|
||||
props.listOptionBaseClass,
|
||||
active ? props.listOptionActiveClass : props.listOptionInactiveClass,
|
||||
selected ? props.listOptionSelectedClass : props.listOptionUnselectedClass,
|
||||
disabled && props.listOptionDisabledClass
|
||||
)
|
||||
}
|
||||
|
||||
function resolveOptionIconClass ({ active }: { active: boolean }) {
|
||||
return classNames(
|
||||
props.listOptionIconBaseClass,
|
||||
active ? props.listOptionIconActiveClass : props.listOptionIconInactiveClass
|
||||
)
|
||||
}
|
||||
|
||||
function onUpdate (event: any) {
|
||||
if (query.value && searchInput.value?.$el) {
|
||||
query.value = ''
|
||||
// explicitly set input text because `ComboboxInput` `displayValue` is not reactive
|
||||
searchInput.value.$el.value = ''
|
||||
}
|
||||
emit('update:modelValue', event)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'USelectCustom' }
|
||||
</script>
|
||||
329
src/runtime/components/forms/SelectMenu.vue
Normal file
329
src/runtime/components/forms/SelectMenu.vue
Normal file
@@ -0,0 +1,329 @@
|
||||
<template>
|
||||
<component
|
||||
:is="searchable ? 'Combobox' : 'Listbox'"
|
||||
v-slot="{ open }"
|
||||
:by="by"
|
||||
:name="name"
|
||||
:model-value="modelValue"
|
||||
:multiple="multiple"
|
||||
:disabled="disabled"
|
||||
as="div"
|
||||
:class="ui.wrapper"
|
||||
@update:model-value="onUpdate"
|
||||
>
|
||||
<!-- TODO: check that `name` fixes required -->
|
||||
<!-- <input :value="modelValue" :required="required" class="absolute inset-0 w-px opacity-0 cursor-default" tabindex="-1" aria-hidden="true"> -->
|
||||
|
||||
<component
|
||||
:is="searchable ? 'ComboboxButton' : 'ListboxButton'"
|
||||
ref="trigger"
|
||||
as="div"
|
||||
role="button"
|
||||
class="inline-flex w-full"
|
||||
>
|
||||
<slot :open="open" :disabled="disabled">
|
||||
<button :class="selectMenuClass" :disabled="disabled" type="button">
|
||||
<span v-if="icon" :class="leadingIconClass">
|
||||
<Icon :name="icon" :class="iconClass" />
|
||||
</span>
|
||||
|
||||
<slot name="label">
|
||||
<span v-if="modelValue" class="block truncate">{{ typeof modelValue === 'string' ? modelValue : (modelValue as any)[optionAttribute] }}</span>
|
||||
<span v-else class="block truncate text-gray-400 dark:text-gray-500">{{ placeholder || ' ' }}</span>
|
||||
</slot>
|
||||
|
||||
<span :class="trailingIconClass">
|
||||
<Icon name="i-heroicons-chevron-down-20-solid" :class="iconClass" aria-hidden="true" />
|
||||
</span>
|
||||
</button>
|
||||
</slot>
|
||||
</component>
|
||||
|
||||
<div v-if="open" ref="container" :class="[ui.container, ui.width]">
|
||||
<transition v-bind="ui.transition">
|
||||
<component :is="searchable ? 'ComboboxOptions' : 'ListboxOptions'" static :class="[ui.base, ui.divide, ui.ring, ui.rounded, ui.shadow, ui.background, ui.spacing, ui.height]">
|
||||
<ComboboxInput
|
||||
v-if="searchable"
|
||||
ref="searchInput"
|
||||
:display-value="() => query"
|
||||
name="q"
|
||||
placeholder="Search..."
|
||||
autofocus
|
||||
autocomplete="off"
|
||||
:class="ui.input"
|
||||
@change="query = $event.target.value"
|
||||
/>
|
||||
<component
|
||||
:is="searchable ? 'ComboboxOption' : 'ListboxOption'"
|
||||
v-for="(option, index) in filteredOptions"
|
||||
v-slot="{ active, selected, disabled: optionDisabled }"
|
||||
:key="index"
|
||||
as="template"
|
||||
:value="option"
|
||||
:disabled="option.disabled"
|
||||
>
|
||||
<li :class="resolveOptionClass({ active, disabled: optionDisabled })">
|
||||
<div :class="ui.option.container">
|
||||
<slot name="option" :option="option" :active="active" :selected="selected">
|
||||
<Icon v-if="option.icon" :name="option.icon" :class="[ui.option.icon.base, active ? ui.option.icon.active : ui.option.icon.inactive, option.iconClass]" aria-hidden="true" />
|
||||
<Avatar
|
||||
v-else-if="option.avatar"
|
||||
v-bind="{ size: ui.option.avatar.size, ...option.avatar }"
|
||||
:class="ui.option.avatar.base"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span v-else-if="option.chip" :class="ui.option.chip.base" :style="{ background: `#${option.chip}` }" />
|
||||
|
||||
<span class="truncate">{{ typeof option === 'string' ? option : option[optionAttribute] }}</span>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<span v-if="selected" :class="ui.option.selected.wrapper">
|
||||
<Icon :name="selectedIcon" :class="ui.option.selected.icon" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</component>
|
||||
|
||||
<component :is="searchable ? 'ComboboxOption' : 'ListboxOption'" v-if="creatable && queryOption && !filteredOptions.length" v-slot="{ active, selected }" :value="queryOption" as="template">
|
||||
<li :class="resolveOptionClass({ active })">
|
||||
<div :class="ui.option.container">
|
||||
<slot name="option-create" :option="queryOption" :active="active" :selected="selected">
|
||||
<span class="block truncate">Create "{{ queryOption[optionAttribute] }}"</span>
|
||||
</slot>
|
||||
</div>
|
||||
</li>
|
||||
</component>
|
||||
<p v-else-if="searchable && query && !filteredOptions.length" :class="ui.option.empty">
|
||||
<slot name="option-empty" :query="query">
|
||||
No results found for "{{ query }}".
|
||||
</slot>
|
||||
</p>
|
||||
</component>
|
||||
</transition>
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, computed, watch, defineComponent } from 'vue'
|
||||
import type { PropType, ComponentPublicInstance } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { Combobox, ComboboxButton, ComboboxOptions, ComboboxOption, ComboboxInput, Listbox, ListboxButton, ListboxOptions, ListboxOption } from '@headlessui/vue'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import Avatar from '../elements/Avatar.vue'
|
||||
import { classNames } from '../../utils'
|
||||
import { usePopper } from '../../composables/usePopper'
|
||||
import type { PopperOptions } from '../../types'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Combobox,
|
||||
ComboboxButton,
|
||||
ComboboxOptions,
|
||||
ComboboxOption,
|
||||
ComboboxInput,
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxOptions,
|
||||
ListboxOption,
|
||||
Icon,
|
||||
Avatar
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [String, Number, Object, Array],
|
||||
default: ''
|
||||
},
|
||||
by: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
options: {
|
||||
type: Array as PropType<{ [key: string]: any, disabled?: boolean }[] | string[]>,
|
||||
default: () => []
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
selectedIcon: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.selectMenu.default.selectedIcon
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
searchable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
creatable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.select.default.size,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.select.size).includes(value)
|
||||
}
|
||||
},
|
||||
appearance: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.select.default.appearance,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.select.appearance).includes(value)
|
||||
}
|
||||
},
|
||||
optionAttribute: {
|
||||
type: String,
|
||||
default: 'label'
|
||||
},
|
||||
searchAttributes: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
popper: {
|
||||
type: Object as PropType<PopperOptions>,
|
||||
default: () => ({})
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.selectMenu>>,
|
||||
default: () => appConfig.ui.selectMenu
|
||||
},
|
||||
uiSelect: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.select>>,
|
||||
default: () => appConfig.ui.select
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'open', 'close'],
|
||||
setup (props, { emit }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.selectMenu>>(() => defu({}, props.ui, appConfig.ui.selectMenu))
|
||||
const uiSelect = computed<Partial<typeof appConfig.ui.select>>(() => defu({}, props.uiSelect, appConfig.ui.select))
|
||||
|
||||
const popper = computed<PopperOptions>(() => defu({}, props.popper, ui.value.popper as PopperOptions))
|
||||
|
||||
const [trigger, container] = usePopper(popper.value)
|
||||
|
||||
const query = ref('')
|
||||
const searchInput = ref<ComponentPublicInstance<HTMLElement>>()
|
||||
|
||||
const selectMenuClass = computed(() => {
|
||||
return classNames(
|
||||
uiSelect.value.base,
|
||||
'text-left cursor-default',
|
||||
uiSelect.value.size[props.size],
|
||||
uiSelect.value.gap[props.size],
|
||||
uiSelect.value.spacing[props.size],
|
||||
uiSelect.value.appearance[props.appearance],
|
||||
!!props.icon && uiSelect.value.leading.spacing[props.size],
|
||||
uiSelect.value.trailing.spacing[props.size],
|
||||
uiSelect.value.custom,
|
||||
'inline-flex items-center'
|
||||
)
|
||||
})
|
||||
|
||||
const iconClass = computed(() => {
|
||||
return classNames(
|
||||
uiSelect.value.icon.base,
|
||||
uiSelect.value.icon.size[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
const leadingIconClass = computed(() => {
|
||||
return classNames(
|
||||
uiSelect.value.icon.leading.wrapper,
|
||||
uiSelect.value.icon.leading.spacing[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
const trailingIconClass = computed(() => {
|
||||
return classNames(
|
||||
uiSelect.value.icon.trailing.wrapper,
|
||||
uiSelect.value.icon.trailing.spacing[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
const filteredOptions = computed(() =>
|
||||
query.value === ''
|
||||
? props.options
|
||||
: (props.options as any[]).filter((option: any) => {
|
||||
return (props.searchAttributes?.length ? props.searchAttributes : [props.optionAttribute]).some((searchAttribute: any) => {
|
||||
return typeof option === 'string' ? option.search(new RegExp(query.value, 'i')) !== -1 : (option[searchAttribute] && option[searchAttribute].search(new RegExp(query.value, 'i')) !== -1)
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
const queryOption = computed(() => {
|
||||
return query.value === '' ? null : { [props.optionAttribute]: query.value }
|
||||
})
|
||||
|
||||
watch(container, (value) => {
|
||||
if (value) {
|
||||
emit('open')
|
||||
} else {
|
||||
emit('close')
|
||||
}
|
||||
})
|
||||
|
||||
function resolveOptionClass ({ active, disabled }: { active: boolean, disabled?: boolean }) {
|
||||
return classNames(
|
||||
ui.value.option.base,
|
||||
active ? ui.value.option.active : ui.value.option.inactive,
|
||||
disabled && ui.value.option.disabled
|
||||
)
|
||||
}
|
||||
|
||||
function onUpdate (event: any) {
|
||||
if (query.value && searchInput.value?.$el) {
|
||||
query.value = ''
|
||||
// explicitly set input text because `ComboboxInput` `displayValue` is not reactive
|
||||
searchInput.value.$el.value = ''
|
||||
}
|
||||
emit('update:modelValue', event)
|
||||
}
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
trigger,
|
||||
container,
|
||||
selectMenuClass,
|
||||
iconClass,
|
||||
leadingIconClass,
|
||||
trailingIconClass,
|
||||
filteredOptions,
|
||||
queryOption,
|
||||
query,
|
||||
resolveOptionClass,
|
||||
onUpdate
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<div :class="ui.wrapper">
|
||||
<textarea
|
||||
:id="name"
|
||||
ref="textarea"
|
||||
@@ -18,141 +18,150 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, nextTick } from 'vue'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
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)
|
||||
}
|
||||
},
|
||||
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 emit = defineEmits(['update:modelValue', 'focus', 'blur'])
|
||||
|
||||
const textarea = ref<HTMLTextAreaElement | null>(null)
|
||||
|
||||
const autoFocus = () => {
|
||||
if (props.autofocus) {
|
||||
textarea.value?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const autoResize = () => {
|
||||
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
|
||||
|
||||
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>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UTextarea' }
|
||||
import { ref, computed, watch, onMounted, nextTick, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { classNames } from '../../utils'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
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
|
||||
},
|
||||
resize: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.textarea.default.size,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.textarea.size).includes(value)
|
||||
}
|
||||
},
|
||||
appearance: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.textarea.default.appearance,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.textarea.appearance).includes(value)
|
||||
}
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.textarea>>,
|
||||
default: () => appConfig.ui.textarea
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'focus', 'blur'],
|
||||
setup (props, { emit }) {
|
||||
const textarea = ref<HTMLTextAreaElement | null>(null)
|
||||
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.textarea>>(() => defu({}, props.ui, appConfig.ui.textarea))
|
||||
|
||||
const autoFocus = () => {
|
||||
if (props.autofocus) {
|
||||
textarea.value?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const autoResize = () => {
|
||||
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
|
||||
|
||||
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(
|
||||
ui.value.base,
|
||||
ui.value.size[props.size],
|
||||
ui.value.spacing[props.size],
|
||||
ui.value.appearance[props.appearance],
|
||||
!props.resize && 'resize-none',
|
||||
ui.value.custom
|
||||
)
|
||||
})
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
textareaClass,
|
||||
onInput
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,96 +1,77 @@
|
||||
<template>
|
||||
<Switch
|
||||
v-model="active"
|
||||
:class="[active ? activeClass : inactiveClass, baseClass]"
|
||||
:class="[active ? ui.active : ui.inactive, ui.base]"
|
||||
>
|
||||
<span :class="[active ? containerActiveClass : containerInactiveClass, containerBaseClass]">
|
||||
<span v-if="iconOn" :class="[active ? iconActiveClass : iconInactiveClass, iconBaseClass]" aria-hidden="true">
|
||||
<Icon :name="iconOn" :class="iconOnClass" />
|
||||
<span :class="[active ? ui.container.active : ui.container.inactive, ui.container.base]">
|
||||
<span v-if="iconOn" :class="[active ? ui.icon.active : ui.icon.inactive, ui.icon.base]" aria-hidden="true">
|
||||
<Icon :name="iconOn" :class="ui.icon.on" />
|
||||
</span>
|
||||
<span v-if="iconOff" :class="[active ? iconInactiveClass : iconActiveClass, iconBaseClass]" aria-hidden="true">
|
||||
<Icon :name="iconOff" :class="iconOffClass" />
|
||||
<span v-if="iconOff" :class="[active ? ui.icon.active : ui.icon.inactive, ui.icon.base]" aria-hidden="true">
|
||||
<Icon :name="iconOff" :class="ui.icon.off" />
|
||||
</span>
|
||||
</span>
|
||||
</Switch>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { Switch } from '@headlessui/vue'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import $ui from '#build/ui'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
iconOn: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
iconOff: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.toggle.base
|
||||
},
|
||||
activeClass: {
|
||||
type: String,
|
||||
default: () => $ui.toggle.active
|
||||
},
|
||||
inactiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.toggle.inactive
|
||||
},
|
||||
containerBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.toggle.container.base
|
||||
},
|
||||
containerActiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.toggle.container.active
|
||||
},
|
||||
containerInactiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.toggle.container.inactive
|
||||
},
|
||||
iconBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.toggle.icon.base
|
||||
},
|
||||
iconActiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.toggle.icon.active
|
||||
},
|
||||
iconInactiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.toggle.icon.inactive
|
||||
},
|
||||
iconOnClass: {
|
||||
type: String,
|
||||
default: () => $ui.toggle.icon.on
|
||||
},
|
||||
iconOffClass: {
|
||||
type: String,
|
||||
default: () => $ui.toggle.icon.off
|
||||
}
|
||||
})
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const active = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
export default defineComponent({
|
||||
components: {
|
||||
// eslint-disable-next-line vue/no-reserved-component-names
|
||||
Switch,
|
||||
Icon
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
iconOn: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
iconOff: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.toggle>>,
|
||||
default: () => appConfig.ui.toggle
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup (props, { emit }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.toggle>>(() => defu({}, props.ui, appConfig.ui.toggle))
|
||||
|
||||
const active = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
active
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UToggle' }
|
||||
</script>
|
||||
|
||||
@@ -1,114 +1,50 @@
|
||||
<template>
|
||||
<component
|
||||
:is="$attrs.onSubmit ? 'form': 'div'"
|
||||
:class="cardClass"
|
||||
:class="[ui.base, ui.rounded, ui.divide, ui.ring, ui.shadow, ui.background]"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<div
|
||||
v-if="$slots.header"
|
||||
:class="[headerClass, headerBackgroundClass, borderColorClass, !!$slots.default && 'border-b']"
|
||||
>
|
||||
<div v-if="$slots.header" :class="[ui.header.base, ui.header.spacing, ui.header.background]">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
<div :class="[bodyClass, bodyBackgroundClass]">
|
||||
<div :class="[ui.body.base, ui.body.spacing, ui.body.background]">
|
||||
<slot />
|
||||
</div>
|
||||
<div
|
||||
v-if="$slots.footer"
|
||||
:class="[footerClass, footerBackgroundClass, borderColorClass, (!!$slots.default || (!$slots.default && !!$slots.header)) && 'border-t']"
|
||||
>
|
||||
<div v-if="$slots.footer" :class="[ui.footer.base, ui.footer.spacing, ui.footer.background]">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
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)
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.card>>,
|
||||
default: () => appConfig.ui.card
|
||||
}
|
||||
},
|
||||
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) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.card>>(() => defu({}, props.ui, appConfig.ui.card))
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const cardClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
props.padded && props.rounded && props.roundedClass,
|
||||
!props.padded && props.rounded && props.roundedClass && `sm:${props.roundedClass}`,
|
||||
props.ringClass,
|
||||
props.shadowClass,
|
||||
props.backgroundClass,
|
||||
props.customClass
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'UCard',
|
||||
inheritAttrs: false
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,38 +1,37 @@
|
||||
<template>
|
||||
<div :class="containerClass">
|
||||
<div :class="[ui.base, ui.spacing, ui.constrained]">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
const props = defineProps({
|
||||
padded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.container>>,
|
||||
default: () => appConfig.ui.container
|
||||
}
|
||||
},
|
||||
constrained: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
constrainedClass: {
|
||||
type: String,
|
||||
default: () => $ui.container.constrained
|
||||
setup (props) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.container>>(() => defu({}, props.ui, appConfig.ui.container))
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const containerClass = computed(() => {
|
||||
return classNames(
|
||||
'mx-auto sm:px-6 lg:px-8',
|
||||
props.padded && 'px-4',
|
||||
props.constrained && props.constrainedClass
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UContainer' }
|
||||
</script>
|
||||
|
||||
@@ -7,24 +7,22 @@
|
||||
:nullable="nullable"
|
||||
@update:model-value="onSelect"
|
||||
>
|
||||
<div :class="$ui.commandPalette.wrapper">
|
||||
<div v-show="searchable" class="relative flex items-center">
|
||||
<Icon v-if="inputIcon" :name="inputIcon" :class="$ui.commandPalette.input.icon.base" aria-hidden="true" />
|
||||
<div :class="ui.wrapper">
|
||||
<div v-if="searchable" :class="ui.input.wrapper">
|
||||
<Icon v-if="icon" :name="icon" :class="ui.input.icon" aria-hidden="true" />
|
||||
<ComboboxInput
|
||||
ref="comboboxInput"
|
||||
:value="query"
|
||||
:class="$ui.commandPalette.input.base"
|
||||
:placeholder="inputPlaceholder"
|
||||
:class="[ui.input.base, icon && ui.input.spacing]"
|
||||
:placeholder="placeholder"
|
||||
autocomplete="off"
|
||||
@change="query = $event.target.value"
|
||||
/>
|
||||
|
||||
<Button
|
||||
v-if="inputCloseIcon"
|
||||
:icon="inputCloseIcon"
|
||||
:class="$ui.commandPalette.input.close.base"
|
||||
:size="$ui.commandPalette.input.close.size"
|
||||
:variant="$ui.commandPalette.input.close.variant"
|
||||
v-if="close"
|
||||
v-bind="close"
|
||||
:class="ui.input.close"
|
||||
aria-label="Close"
|
||||
@click="onClear"
|
||||
/>
|
||||
@@ -36,7 +34,7 @@
|
||||
hold
|
||||
as="div"
|
||||
aria-label="Commands"
|
||||
class="relative flex-1 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-800 scroll-py-2"
|
||||
:class="ui.container"
|
||||
>
|
||||
<CommandPaletteGroup
|
||||
v-for="group of groups"
|
||||
@@ -45,6 +43,8 @@
|
||||
:group="group"
|
||||
:group-attribute="groupAttribute"
|
||||
:command-attribute="commandAttribute"
|
||||
:selected-icon="selectedIcon"
|
||||
:ui="ui"
|
||||
>
|
||||
<template v-for="(_, name) in $slots" #[name]="slotData">
|
||||
<slot :name="name" v-bind="slotData" />
|
||||
@@ -52,18 +52,18 @@
|
||||
</CommandPaletteGroup>
|
||||
</ComboboxOptions>
|
||||
|
||||
<div v-else-if="placeholder" class="flex flex-col items-center justify-center flex-1 px-6 py-14 sm:px-14">
|
||||
<Icon v-if="emptyIcon" :name="emptyIcon" class="w-6 h-6 mx-auto u-text-gray-400 mb-4" aria-hidden="true" />
|
||||
<p class="text-sm text-center u-text-gray-900">
|
||||
{{ query ? "We couldn't find any items with that term. Please try again." : "We couldn't find any items." }}
|
||||
<div v-else-if="empty" :class="ui.empty.wrapper">
|
||||
<Icon v-if="empty.icon" :name="empty.icon" :class="ui.empty.icon" aria-hidden="true" />
|
||||
<p :class="query ? ui.empty.queryLabel : ui.empty.label">
|
||||
{{ query ? empty.queryLabel : empty.label }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Combobox>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
<script lang="ts">
|
||||
import { ref, computed, watch, onMounted, defineComponent } from 'vue'
|
||||
import { Combobox, ComboboxInput, ComboboxOptions } from '@headlessui/vue'
|
||||
import type { ComputedRef, PropType, ComponentPublicInstance } from 'vue'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
@@ -74,196 +74,228 @@ import type { UseFuseOptions } from '@vueuse/integrations/useFuse'
|
||||
import type { Group, Command } from '../../types/command-palette'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import Button from '../elements/Button.vue'
|
||||
import type { Button as ButtonType } from '../../types/button'
|
||||
import CommandPaletteGroup from './CommandPaletteGroup.vue'
|
||||
import $ui from '#build/ui'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number, Object, Array],
|
||||
default: null
|
||||
},
|
||||
by: {
|
||||
type: String,
|
||||
default: 'id'
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
nullable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
searchable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
groups: {
|
||||
type: Array as PropType<Group[]>,
|
||||
default: () => []
|
||||
},
|
||||
inputIcon: {
|
||||
type: String,
|
||||
default: () => $ui.commandPalette.input.icon.name
|
||||
},
|
||||
inputCloseIcon: {
|
||||
type: String,
|
||||
default: () => $ui.commandPalette.input.close.icon.name
|
||||
},
|
||||
inputPlaceholder: {
|
||||
type: String,
|
||||
default: 'Search...'
|
||||
},
|
||||
emptyIcon: {
|
||||
type: String,
|
||||
default: () => $ui.commandPalette.empty.icon.name
|
||||
},
|
||||
groupAttribute: {
|
||||
type: String,
|
||||
default: 'label'
|
||||
},
|
||||
commandAttribute: {
|
||||
type: String,
|
||||
default: 'label'
|
||||
},
|
||||
options: {
|
||||
type: Object as PropType<Partial<UseFuseOptions<Command>>>,
|
||||
default: () => ({})
|
||||
},
|
||||
autoselect: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
autoclear: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
placeholder: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
debounce: {
|
||||
type: Number,
|
||||
default: 200
|
||||
}
|
||||
})
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'close'])
|
||||
|
||||
const query = ref('')
|
||||
const comboboxInput = ref<ComponentPublicInstance<HTMLInputElement>>()
|
||||
const comboboxApi = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
if (props.autoselect) {
|
||||
activateFirstOption()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
// @ts-expect-error internals
|
||||
const popoverProvides = comboboxInput.value?.$.provides
|
||||
if (!popoverProvides) {
|
||||
return
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Combobox,
|
||||
ComboboxInput,
|
||||
ComboboxOptions,
|
||||
Icon,
|
||||
// eslint-disable-next-line vue/no-reserved-component-names
|
||||
Button,
|
||||
CommandPaletteGroup
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [String, Number, Object, Array],
|
||||
default: null
|
||||
},
|
||||
by: {
|
||||
type: String,
|
||||
default: 'id'
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
nullable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
searchable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
groups: {
|
||||
type: Array as PropType<Group[]>,
|
||||
default: () => []
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.commandPalette.default.icon
|
||||
},
|
||||
selectedIcon: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.commandPalette.default.selectedIcon
|
||||
},
|
||||
close: {
|
||||
type: Object as PropType<Partial<ButtonType>>,
|
||||
default: () => appConfig.ui.commandPalette.default.close
|
||||
},
|
||||
empty: {
|
||||
type: Object as PropType<{ icon: string, label: string, queryLabel: string }>,
|
||||
default: () => appConfig.ui.commandPalette.default.empty
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'Search...'
|
||||
},
|
||||
groupAttribute: {
|
||||
type: String,
|
||||
default: 'label'
|
||||
},
|
||||
commandAttribute: {
|
||||
type: String,
|
||||
default: 'label'
|
||||
},
|
||||
autoselect: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
autoclear: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
debounce: {
|
||||
type: Number,
|
||||
default: 200
|
||||
},
|
||||
fuse: {
|
||||
type: Object as PropType<Partial<UseFuseOptions<Command>>>,
|
||||
default: () => ({})
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.commandPalette>>,
|
||||
default: () => appConfig.ui.commandPalette
|
||||
}
|
||||
const popoverProvidesSymbols = Object.getOwnPropertySymbols(popoverProvides)
|
||||
comboboxApi.value = popoverProvidesSymbols.length && popoverProvides[popoverProvidesSymbols[0]]
|
||||
}, 200)
|
||||
})
|
||||
|
||||
const options: ComputedRef<Partial<UseFuseOptions<Command>>> = computed(() => defu({}, props.options, {
|
||||
fuseOptions: {
|
||||
keys: [props.commandAttribute]
|
||||
},
|
||||
resultLimit: 12,
|
||||
matchAllWhenSearchEmpty: true
|
||||
}))
|
||||
emits: ['update:modelValue', 'close'],
|
||||
setup (props, { emit, expose }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const commands = computed(() => props.groups.filter(group => !group.search).reduce((acc, group) => {
|
||||
return acc.concat(group.commands.map(command => ({ ...command, group: group.key })))
|
||||
}, [] as Command[]))
|
||||
const ui = computed<Partial<typeof appConfig.ui.commandPalette>>(() => defu({}, props.ui, appConfig.ui.commandPalette))
|
||||
|
||||
const searchResults = ref<{ [key: string]: any }>({})
|
||||
const query = ref('')
|
||||
const comboboxInput = ref<ComponentPublicInstance<HTMLInputElement>>()
|
||||
const comboboxApi = ref(null)
|
||||
|
||||
const { results } = useFuse(query, commands, options)
|
||||
|
||||
const groups = computed(() => ([
|
||||
...map(groupBy(results.value, command => command.item.group), (results, key) => {
|
||||
const commands = results.map((result) => {
|
||||
const { item, ...data } = result
|
||||
|
||||
return {
|
||||
...item,
|
||||
...data
|
||||
onMounted(() => {
|
||||
if (props.autoselect) {
|
||||
activateFirstOption()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
// @ts-expect-error internals
|
||||
const popoverProvides = comboboxInput.value?.$.provides
|
||||
if (!popoverProvides) {
|
||||
return
|
||||
}
|
||||
const popoverProvidesSymbols = Object.getOwnPropertySymbols(popoverProvides)
|
||||
comboboxApi.value = popoverProvidesSymbols.length && popoverProvides[popoverProvidesSymbols[0]]
|
||||
}, 200)
|
||||
})
|
||||
|
||||
const options: ComputedRef<Partial<UseFuseOptions<Command>>> = computed(() => defu({}, props.fuse, {
|
||||
fuseOptions: {
|
||||
keys: [props.commandAttribute]
|
||||
},
|
||||
resultLimit: 12,
|
||||
matchAllWhenSearchEmpty: true
|
||||
}))
|
||||
|
||||
const commands = computed(() => props.groups.filter(group => !group.search).reduce((acc, group) => {
|
||||
return acc.concat(group.commands.map(command => ({ ...command, group: group.key })))
|
||||
}, [] as Command[]))
|
||||
|
||||
const searchResults = ref<{ [key: string]: any }>({})
|
||||
|
||||
const { results } = useFuse(query, commands, options)
|
||||
|
||||
const groups = computed(() => ([
|
||||
...map(groupBy(results.value, command => command.item.group), (results, key) => {
|
||||
const commands = results.map((result) => {
|
||||
const { item, ...data } = result
|
||||
|
||||
return {
|
||||
...item,
|
||||
...data
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
...props.groups.find(group => group.key === key),
|
||||
commands: commands.slice(0, options.value.resultLimit)
|
||||
} as Group
|
||||
}),
|
||||
...props.groups.filter(group => !!group.search).map(group => ({ ...group, commands: (searchResults.value[group.key] || []).slice(0, options.value.resultLimit) })).filter(group => group.commands.length)
|
||||
]))
|
||||
|
||||
const debouncedSearch = useDebounceFn(async () => {
|
||||
const searchableGroups = props.groups.filter(group => !!group.search)
|
||||
|
||||
await Promise.all(searchableGroups.map(async (group) => {
|
||||
searchResults.value[group.key] = await group.search(query.value)
|
||||
}))
|
||||
}, props.debounce)
|
||||
|
||||
watch(query, () => {
|
||||
debouncedSearch()
|
||||
|
||||
// Select first item on search changes
|
||||
setTimeout(() => {
|
||||
// https://github.com/tailwindlabs/headlessui/blob/6fa6074cd5d3a96f78a2d965392aa44101f5eede/packages/%40headlessui-vue/src/components/combobox/combobox.ts#L804
|
||||
comboboxInput.value?.$el.dispatchEvent(new KeyboardEvent('keydown', { key: 'PageUp' }))
|
||||
}, 0)
|
||||
})
|
||||
|
||||
// Methods
|
||||
|
||||
function activateFirstOption () {
|
||||
// hack combobox by using keyboard event
|
||||
// https://github.com/tailwindlabs/headlessui/blob/6fa6074cd5d3a96f78a2d965392aa44101f5eede/packages/%40headlessui-vue/src/components/combobox/combobox.ts#L769
|
||||
setTimeout(() => {
|
||||
comboboxInput.value?.$el.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }))
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function onSelect (option: Command | Command[]) {
|
||||
emit('update:modelValue', option, { query: query.value })
|
||||
|
||||
// Clear input after selection
|
||||
if (props.autoclear) {
|
||||
setTimeout(() => {
|
||||
query.value = ''
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
function onClear () {
|
||||
if (query.value) {
|
||||
query.value = ''
|
||||
} else {
|
||||
emit('close')
|
||||
}
|
||||
}
|
||||
|
||||
expose({
|
||||
query,
|
||||
updateQuery: (q: string) => {
|
||||
query.value = q
|
||||
},
|
||||
comboboxApi,
|
||||
results
|
||||
})
|
||||
|
||||
return {
|
||||
...props.groups.find(group => group.key === key),
|
||||
commands: commands.slice(0, options.value.resultLimit)
|
||||
} as Group
|
||||
}),
|
||||
...props.groups.filter(group => !!group.search).map(group => ({ ...group, commands: (searchResults.value[group.key] || []).slice(0, options.value.resultLimit) })).filter(group => group.commands.length)
|
||||
]))
|
||||
|
||||
const debouncedSearch = useDebounceFn(async () => {
|
||||
const searchableGroups = props.groups.filter(group => !!group.search)
|
||||
|
||||
await Promise.all(searchableGroups.map(async (group) => {
|
||||
searchResults.value[group.key] = await group.search(query.value)
|
||||
}))
|
||||
}, props.debounce)
|
||||
|
||||
watch(query, () => {
|
||||
debouncedSearch()
|
||||
|
||||
// Select first item on search changes
|
||||
setTimeout(() => {
|
||||
// https://github.com/tailwindlabs/headlessui/blob/6fa6074cd5d3a96f78a2d965392aa44101f5eede/packages/%40headlessui-vue/src/components/combobox/combobox.ts#L804
|
||||
comboboxInput.value?.$el.dispatchEvent(new KeyboardEvent('keydown', { key: 'PageUp' }))
|
||||
}, 0)
|
||||
})
|
||||
|
||||
// Methods
|
||||
|
||||
function activateFirstOption () {
|
||||
// hack combobox by using keyboard event
|
||||
// https://github.com/tailwindlabs/headlessui/blob/6fa6074cd5d3a96f78a2d965392aa44101f5eede/packages/%40headlessui-vue/src/components/combobox/combobox.ts#L769
|
||||
setTimeout(() => {
|
||||
comboboxInput.value?.$el.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }))
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function onSelect (option: Command | Command[]) {
|
||||
emit('update:modelValue', option, { query: query.value })
|
||||
|
||||
// Clear input after selection
|
||||
if (props.autoclear) {
|
||||
setTimeout(() => {
|
||||
query.value = ''
|
||||
}, 0)
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
groups,
|
||||
query,
|
||||
onSelect,
|
||||
onClear
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onClear () {
|
||||
if (query.value) {
|
||||
query.value = ''
|
||||
} else {
|
||||
emit('close')
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
query,
|
||||
updateQuery: (q: string) => {
|
||||
query.value = q
|
||||
},
|
||||
comboboxApi,
|
||||
results
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UCommandPalette' }
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="p-2" role="option">
|
||||
<h2 v-if="label" class="px-3 my-2 text-xs font-semibold u-text-gray-900">
|
||||
<div :class="ui.group.wrapper" role="option">
|
||||
<h2 v-if="label" :class="ui.group.label">
|
||||
{{ label }}
|
||||
</h2>
|
||||
|
||||
<div class="text-sm u-text-gray-700" role="listbox" :aria-label="group[groupAttribute]">
|
||||
<div :class="ui.group.container" role="listbox" :aria-label="group[groupAttribute]">
|
||||
<ComboboxOption
|
||||
v-for="(command, index) of group.commands"
|
||||
:key="`${group.key}-${index}`"
|
||||
@@ -13,41 +13,41 @@
|
||||
:disabled="command.disabled"
|
||||
as="template"
|
||||
>
|
||||
<div :class="['flex justify-between select-none items-center rounded-md px-3 py-2 gap-3 relative', active && 'bg-gray-100 dark:bg-gray-800 u-text-gray-900', command.disabled ? 'cursor-not-allowed' : 'cursor-pointer']">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<div :class="[ui.group.command.base, active ? ui.group.command.active : ui.group.command.inactive, command.disabled ? 'cursor-not-allowed' : 'cursor-pointer']">
|
||||
<div :class="ui.group.command.container">
|
||||
<slot :name="`${group.key}-icon`" :group="group" :command="command">
|
||||
<Icon v-if="command.icon" :name="command.icon" :class="['h-4 w-4 flex-shrink-0', active ? 'text-opacity-100 dark:text-opacity-100' : 'text-opacity-40 dark:text-opacity-40', command.iconClass || 'text-gray-900 dark:text-gray-50']" aria-hidden="true" />
|
||||
<Icon v-if="command.icon" :name="command.icon" :class="[ui.group.command.icon.base, active ? ui.group.command.icon.active : ui.group.command.icon.inactive, command.iconClass]" aria-hidden="true" />
|
||||
<Avatar
|
||||
v-else-if="command.avatar"
|
||||
v-bind="{ size: 'xxxs', ...command.avatar }"
|
||||
class="flex-shrink-0"
|
||||
v-bind="{ size: ui.group.command.avatar.size, ...command.avatar }"
|
||||
:class="ui.group.command.avatar.base"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span v-else-if="command.chip" class="flex-shrink-0 w-2 h-2 mx-1 rounded-full" :style="{ background: `#${command.chip}` }" />
|
||||
<span v-else-if="command.chip" :class="ui.group.command.chip.base" :style="{ background: `#${command.chip}` }" />
|
||||
</slot>
|
||||
|
||||
<div class="flex items-center gap-1.5 min-w-0" :class="{ 'opacity-50': command.disabled }">
|
||||
<div :class="[ui.group.command.label, command.disabled && ui.group.command.disabled]">
|
||||
<slot :name="`${group.key}-command`" :group="group" :command="command">
|
||||
<span v-if="command.prefix" class="flex-shrink-0" :class="command.prefixClass || 'u-text-gray-400'">{{ command.prefix }}</span>
|
||||
<span v-if="command.prefix" class="flex-shrink-0" :class="command.prefixClass || ui.group.command.prefix">{{ command.prefix }}</span>
|
||||
|
||||
<span class="truncate" :class="{ 'flex-none': command.suffix || command.matches?.length }">{{ command[commandAttribute] }}</span>
|
||||
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span v-if="command.matches?.length" class="truncate" :class="command.suffixClass || 'u-text-gray-400'" v-html="highlight(command[commandAttribute], command.matches[0])" />
|
||||
<span v-else-if="command.suffix" class="truncate" :class="command.suffixClass || 'u-text-gray-400'">{{ command.suffix }}</span>
|
||||
<span v-if="command.matches?.length" class="truncate" :class="command.suffixClass || ui.group.command.suffix" v-html="highlight(command[commandAttribute], command.matches[0])" />
|
||||
<span v-else-if="command.suffix" class="truncate" :class="command.suffixClass || ui.group.command.suffix">{{ command.suffix }}</span>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Icon v-if="selected" :name="$ui.commandPalette.option.selected.icon.name" class="h-5 w-5 u-text-gray-900 flex-shrink-0" aria-hidden="true" />
|
||||
<Icon v-if="selected" :name="selectedIcon" :class="ui.group.command.selected.icon" aria-hidden="true" />
|
||||
<slot v-else-if="active && (group.active || $slots[`${group.key}-active`])" :name="`${group.key}-active`" :group="group" :command="command">
|
||||
<span v-if="group.active" class="flex-shrink-0 u-text-gray-500">{{ group.active }}</span>
|
||||
<span v-if="group.active" :class="ui.group.active">{{ group.active }}</span>
|
||||
</slot>
|
||||
<slot v-else :name="`${group.key}-inactive`" :group="group" :command="command">
|
||||
<span v-if="command.shortcuts?.length" :class="$ui.commandPalette.option.shortcuts">
|
||||
<span v-if="command.shortcuts?.length" :class="ui.group.command.shortcuts">
|
||||
<kbd v-for="shortcut of command.shortcuts" :key="shortcut" class="font-sans">{{ shortcut }}</kbd>
|
||||
</span>
|
||||
<span v-else-if="!command.disabled && group.inactive" class="flex-shrink-0 u-text-gray-500">{{ group.inactive }}</span>
|
||||
<span v-else-if="!command.disabled && group.inactive" :class="ui.group.inactive">{{ group.inactive }}</span>
|
||||
</slot>
|
||||
</div>
|
||||
</ComboboxOption>
|
||||
@@ -55,73 +55,94 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ComboboxOption } from '@headlessui/vue'
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { ComboboxOption } from '@headlessui/vue'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import Avatar from '../elements/Avatar.vue'
|
||||
import type { Group } from '../../types/command-palette'
|
||||
import $ui from '#build/ui'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
const props = defineProps({
|
||||
group: {
|
||||
type: Object as PropType<Group>,
|
||||
required: true
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
ComboboxOption,
|
||||
Icon,
|
||||
Avatar
|
||||
},
|
||||
query: {
|
||||
type: String,
|
||||
default: ''
|
||||
props: {
|
||||
group: {
|
||||
type: Object as PropType<Group>,
|
||||
required: true
|
||||
},
|
||||
query: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
groupAttribute: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
commandAttribute: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
selectedIcon: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.commandPalette>>,
|
||||
default: () => appConfig.ui.commandPalette
|
||||
}
|
||||
},
|
||||
groupAttribute: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
commandAttribute: {
|
||||
type: String,
|
||||
required: true
|
||||
setup (props) {
|
||||
const label = computed(() => {
|
||||
const label = props.group[props.groupAttribute]
|
||||
|
||||
return typeof label === 'function' ? label(props.query) : label
|
||||
})
|
||||
|
||||
function highlight (text: string, { indices, value }: { indices: number[][], value:string }): string {
|
||||
if (text === value) {
|
||||
return ''
|
||||
}
|
||||
|
||||
let content = ''
|
||||
let nextUnhighlightedIndiceStartingIndex = 0
|
||||
|
||||
indices.forEach((indice) => {
|
||||
const lastIndiceNextIndex = indice[1] + 1
|
||||
const isMatched = (lastIndiceNextIndex - indice[0]) >= props.query.length
|
||||
|
||||
content += [
|
||||
value.substring(nextUnhighlightedIndiceStartingIndex, indice[0]),
|
||||
isMatched && '<mark>',
|
||||
value.substring(indice[0], lastIndiceNextIndex),
|
||||
isMatched && '</mark>'
|
||||
].filter(Boolean).join('')
|
||||
|
||||
nextUnhighlightedIndiceStartingIndex = lastIndiceNextIndex
|
||||
})
|
||||
|
||||
content += value.substring(nextUnhighlightedIndiceStartingIndex)
|
||||
|
||||
const index = content.indexOf('<mark>')
|
||||
if (index > 60) {
|
||||
content = `...${content.substring(index - 60)}`
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
return {
|
||||
label,
|
||||
highlight
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const label = computed(() => {
|
||||
const label = props.group[props.groupAttribute]
|
||||
|
||||
return typeof label === 'function' ? label(props.query) : label
|
||||
})
|
||||
|
||||
function highlight (text: string, { indices, value }: { indices: number[][], value:string }): string {
|
||||
if (text === value) {
|
||||
return ''
|
||||
}
|
||||
|
||||
let content = ''
|
||||
let nextUnhighlightedIndiceStartingIndex = 0
|
||||
|
||||
indices.forEach((indice) => {
|
||||
const lastIndiceNextIndex = indice[1] + 1
|
||||
const isMatched = (lastIndiceNextIndex - indice[0]) >= props.query.length
|
||||
|
||||
content += [
|
||||
value.substring(nextUnhighlightedIndiceStartingIndex, indice[0]),
|
||||
isMatched && '<mark>',
|
||||
value.substring(indice[0], lastIndiceNextIndex),
|
||||
isMatched && '</mark>'
|
||||
].filter(Boolean).join('')
|
||||
|
||||
nextUnhighlightedIndiceStartingIndex = lastIndiceNextIndex
|
||||
})
|
||||
|
||||
content += value.substring(nextUnhighlightedIndiceStartingIndex)
|
||||
|
||||
const index = content.indexOf('<mark>')
|
||||
if (index > 60) {
|
||||
content = `...${content.substring(index - 60)}`
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UCommandPaletteGroup' }
|
||||
</script>
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<template>
|
||||
<nav :class="wrapperClass">
|
||||
<Link
|
||||
v-for="(link, index) of links"
|
||||
:key="index"
|
||||
:to="link.to"
|
||||
:exact="link.exact"
|
||||
:class="baseClass"
|
||||
:active-class="activeClass"
|
||||
:inactive-class="inactiveClass"
|
||||
>
|
||||
{{ link.label }}
|
||||
</Link>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue'
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
import Link from '../elements/Link.vue'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
defineProps({
|
||||
links: {
|
||||
type: Array as PropType<{ to: RouteLocationNormalized, exact: boolean, label: string }[]>,
|
||||
required: true
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.pills.wrapper
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.pills.base
|
||||
},
|
||||
activeClass: {
|
||||
type: String,
|
||||
default: () => $ui.pills.active
|
||||
},
|
||||
inactiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.pills.inactive
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UPills' }
|
||||
</script>
|
||||
@@ -1,49 +0,0 @@
|
||||
<template>
|
||||
<nav :class="wrapperClass">
|
||||
<Link
|
||||
v-for="(link, index) of links"
|
||||
:key="index"
|
||||
:to="link.to"
|
||||
:exact="link.exact"
|
||||
:class="baseClass"
|
||||
:active-class="activeClass"
|
||||
:inactive-class="inactiveClass"
|
||||
>
|
||||
{{ link.label }}
|
||||
</Link>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue'
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
import Link from '../elements/Link.vue'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
defineProps({
|
||||
links: {
|
||||
type: Array as PropType<{ to: RouteLocationNormalized, exact: boolean, label: string }[]>,
|
||||
required: true
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.tabs.wrapper
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.tabs.base
|
||||
},
|
||||
activeClass: {
|
||||
type: String,
|
||||
default: () => $ui.tabs.active
|
||||
},
|
||||
inactiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.tabs.inactive
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UTabs' }
|
||||
</script>
|
||||
@@ -1,54 +1,67 @@
|
||||
<template>
|
||||
<nav :class="wrapperClass">
|
||||
<Link
|
||||
<nav :class="ui.wrapper">
|
||||
<LinkCustom
|
||||
v-for="(link, index) of links"
|
||||
v-slot="{ isActive }"
|
||||
:key="index"
|
||||
v-bind="link"
|
||||
:class="[baseClass, spacingClass].join(' ')"
|
||||
:active-class="activeClass"
|
||||
:inactive-class="inactiveClass"
|
||||
:class="[ui.base, ui.spacing]"
|
||||
:active-class="ui.active"
|
||||
:inactive-class="ui.inactive"
|
||||
@click="link.click && link.click()"
|
||||
@keyup.enter="$event.target.blur()"
|
||||
>
|
||||
<slot name="avatar" :link="link">
|
||||
<Avatar
|
||||
v-if="link.avatar"
|
||||
v-bind="{ size: 'xs', ...link.avatar }"
|
||||
:class="[avatarBaseClass, link.label && avatarSpacingClass]"
|
||||
v-bind="{ size: ui.avatar.size, ...link.avatar }"
|
||||
:class="[ui.avatar.base]"
|
||||
/>
|
||||
</slot>
|
||||
<slot name="icon" :link="link" :is-active="isActive">
|
||||
<Icon
|
||||
v-if="link.icon"
|
||||
:name="link.icon"
|
||||
:class="[iconBaseClass, link.label && iconSpacingClass, isActive ? iconActiveClass : iconInactiveClass, link.iconClass]"
|
||||
:class="[ui.icon.base, isActive ? ui.icon.active : ui.icon.inactive, link.iconClass]"
|
||||
/>
|
||||
</slot>
|
||||
<slot :link="link">
|
||||
<span v-if="link.label" class="truncate">{{ link.label }}</span>
|
||||
</slot>
|
||||
<slot name="badge" :link="link" :is-active="isActive">
|
||||
<span v-if="link.badge" :class="[badgeBaseClass, isActive ? badgeActiveClass : badgeInactiveClass]">
|
||||
<span v-if="link.badge" :class="[ui.badge.baseClass, isActive ? ui.badge.active : ui.badge.inactive]">
|
||||
{{ link.badge }}
|
||||
</span>
|
||||
</slot>
|
||||
</Link>
|
||||
</LinkCustom>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
import { defu } from 'defu'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import Link from '../elements/Link.vue'
|
||||
import Avatar from '../elements/Avatar.vue'
|
||||
import LinkCustom from '../elements/LinkCustom.vue'
|
||||
import type { Avatar as AvatarType } from '../../types/avatar'
|
||||
import $ui from '#build/ui'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
defineProps({
|
||||
links: {
|
||||
type: Array as PropType<{
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Icon,
|
||||
Avatar,
|
||||
LinkCustom
|
||||
},
|
||||
props: {
|
||||
links: {
|
||||
type: Array as PropType<{
|
||||
to?: RouteLocationNormalized | string
|
||||
exact?: boolean
|
||||
label: string
|
||||
@@ -58,67 +71,23 @@ defineProps({
|
||||
click?: Function
|
||||
badge?: string
|
||||
}[]>,
|
||||
required: true
|
||||
default: () => []
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.verticalNavigation>>,
|
||||
default: () => appConfig.ui.verticalNavigation
|
||||
}
|
||||
},
|
||||
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
|
||||
},
|
||||
avatarBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.avatar.base
|
||||
},
|
||||
avatarSpacingClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.avatar.spacing
|
||||
},
|
||||
badgeBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.badge.base
|
||||
},
|
||||
badgeActiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.badge.active
|
||||
},
|
||||
badgeInactiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.verticalNavigation.badge.inactive
|
||||
setup (props) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.verticalNavigation>>(() => defu({}, props.ui, appConfig.ui.verticalNavigation))
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UVerticalNavigation' }
|
||||
</script>
|
||||
|
||||
@@ -1,91 +1,79 @@
|
||||
<template>
|
||||
<div v-if="isOpen" ref="container" :class="[containerClass, widthClass]">
|
||||
<transition appear v-bind="transitionClass">
|
||||
<div :class="[baseClass, ringClass, roundedClass, shadowClass, backgroundClass]">
|
||||
<div v-if="isOpen" ref="container" :class="[ui.container, ui.width]">
|
||||
<transition appear v-bind="ui.transition">
|
||||
<div :class="[ui.base, ui.ring, ui.rounded, ui.shadow, ui.background]">
|
||||
<slot />
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts">
|
||||
import { computed, toRef, defineComponent } from 'vue'
|
||||
import type { PropType, Ref } from 'vue'
|
||||
import { computed, toRef } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { onClickOutside } from '@vueuse/core'
|
||||
import type { VirtualElement } from '@popperjs/core'
|
||||
import { usePopper } from '../../composables/usePopper'
|
||||
import type { PopperOptions } from '../../types'
|
||||
import $ui from '#build/ui'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
virtualElement: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
popper: {
|
||||
type: Object as PropType<PopperOptions>,
|
||||
default: () => ({})
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.contextMenu>>,
|
||||
default: () => appConfig.ui.contextMenu
|
||||
}
|
||||
},
|
||||
virtualElement: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.contextMenu.wrapper
|
||||
},
|
||||
containerClass: {
|
||||
type: String,
|
||||
default: () => $ui.contextMenu.container
|
||||
},
|
||||
widthClass: {
|
||||
type: String,
|
||||
default: () => $ui.contextMenu.width
|
||||
},
|
||||
backgroundClass: {
|
||||
type: String,
|
||||
default: () => $ui.contextMenu.background
|
||||
},
|
||||
shadowClass: {
|
||||
type: String,
|
||||
default: () => $ui.contextMenu.shadow
|
||||
},
|
||||
roundedClass: {
|
||||
type: String,
|
||||
default: () => $ui.contextMenu.rounded
|
||||
},
|
||||
ringClass: {
|
||||
type: String,
|
||||
default: () => $ui.contextMenu.ring
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.contextMenu.base
|
||||
},
|
||||
transitionClass: {
|
||||
type: Object,
|
||||
default: () => $ui.contextMenu.transition
|
||||
},
|
||||
popperOptions: {
|
||||
type: Object as PropType<PopperOptions>,
|
||||
default: () => ({})
|
||||
emits: ['update:modelValue', 'close'],
|
||||
setup (props, { emit }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.contextMenu>>(() => defu({}, props.ui, appConfig.ui.contextMenu))
|
||||
|
||||
const popper = computed<PopperOptions>(() => defu({}, props.popper, ui.value.popper as PopperOptions))
|
||||
|
||||
const isOpen = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
const virtualElement = toRef(props, 'virtualElement') as Ref<VirtualElement>
|
||||
|
||||
const [, container] = usePopper(popper.value, virtualElement)
|
||||
|
||||
onClickOutside(container, () => {
|
||||
isOpen.value = false
|
||||
})
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
isOpen,
|
||||
container
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'close'])
|
||||
|
||||
const isOpen = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
const virtualElement = toRef(props, 'virtualElement') as Ref<VirtualElement>
|
||||
|
||||
const popperOptions = computed<PopperOptions>(() => defu({}, props.popperOptions, $ui.contextMenu.popperOptions))
|
||||
|
||||
const [, container] = usePopper(popperOptions.value, virtualElement)
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UContextMenu' }
|
||||
</script>
|
||||
|
||||
@@ -1,39 +1,15 @@
|
||||
<template>
|
||||
<TransitionRoot :appear="appear" :show="isOpen" as="template">
|
||||
<Dialog :class="wrapperClass" @close="close">
|
||||
<TransitionChild
|
||||
v-if="overlay"
|
||||
as="template"
|
||||
:appear="appear"
|
||||
v-bind="overlayTransition"
|
||||
>
|
||||
<div class="fixed inset-0 transition-opacity" :class="overlayBackgroundClass" />
|
||||
<Dialog :class="ui.wrapper" @close="close">
|
||||
<TransitionChild v-if="overlay" as="template" :appear="appear" v-bind="ui.overlay.transition">
|
||||
<div :class="[ui.overlay.base, ui.overlay.background]" />
|
||||
</TransitionChild>
|
||||
|
||||
<div :class="innerClass" :style="innerStyle">
|
||||
<div :class="containerClass">
|
||||
<TransitionChild
|
||||
as="template"
|
||||
:appear="appear"
|
||||
v-bind="modalTransition"
|
||||
>
|
||||
<DialogPanel :class="modalClass">
|
||||
<Card
|
||||
base-class=""
|
||||
background-class=""
|
||||
shadow-class=""
|
||||
ring-class=""
|
||||
rounded-class=""
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<template v-if="$slots.header" #header>
|
||||
<slot name="header" />
|
||||
</template>
|
||||
<slot />
|
||||
<template v-if="$slots.footer" #footer>
|
||||
<slot name="footer" />
|
||||
</template>
|
||||
</Card>
|
||||
<div :class="ui.inner">
|
||||
<div :class="[ui.container, ui.spacing]">
|
||||
<TransitionChild as="template" :appear="appear" v-bind="ui.transition">
|
||||
<DialogPanel :class="[ui.base, ui.width, ui.height, ui.background, ui.ring, ui.rounded, ui.shadow]">
|
||||
<slot />
|
||||
</DialogPanel>
|
||||
</TransitionChild>
|
||||
</div>
|
||||
@@ -42,131 +18,75 @@
|
||||
</TransitionRoot>
|
||||
</template>
|
||||
|
||||
<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.vue'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
appear: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.modal.wrapper
|
||||
},
|
||||
innerClass: {
|
||||
type: String,
|
||||
default: () => $ui.modal.inner
|
||||
},
|
||||
innerStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
containerClass: {
|
||||
type: String,
|
||||
default: () => $ui.modal.container
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.modal.base
|
||||
},
|
||||
backgroundClass: {
|
||||
type: String,
|
||||
default: () => $ui.modal.background
|
||||
},
|
||||
overlay: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
overlayBackgroundClass: {
|
||||
type: String,
|
||||
default: () => $ui.modal.overlay.background
|
||||
},
|
||||
overlayTransitionClass: {
|
||||
type: Object,
|
||||
default: () => $ui.modal.overlay.transition
|
||||
},
|
||||
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
|
||||
},
|
||||
transition: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
transitionClass: {
|
||||
type: Object,
|
||||
default: () => $ui.modal.transition
|
||||
}
|
||||
})
|
||||
|
||||
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,
|
||||
props.widthClass,
|
||||
props.backgroundClass,
|
||||
props.shadowClass,
|
||||
props.ringClass,
|
||||
props.roundedClass
|
||||
)
|
||||
})
|
||||
|
||||
const overlayTransition = computed(() => {
|
||||
if (!props.transition) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return props.overlayTransitionClass
|
||||
})
|
||||
|
||||
const modalTransition = computed(() => {
|
||||
if (!props.transition) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return props.transitionClass
|
||||
})
|
||||
|
||||
function close (value: boolean) {
|
||||
isOpen.value = value
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'UModal',
|
||||
inheritAttrs: false
|
||||
}
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { Dialog, DialogPanel, TransitionRoot, TransitionChild } from '@headlessui/vue'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
// eslint-disable-next-line vue/no-reserved-component-names
|
||||
Dialog,
|
||||
DialogPanel,
|
||||
TransitionRoot,
|
||||
TransitionChild
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
appear: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
overlay: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
transition: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.modal>>,
|
||||
default: () => appConfig.ui.modal
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'close'],
|
||||
setup (props, { emit }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.modal>>(() => defu({}, props.ui, appConfig.ui.modal))
|
||||
|
||||
const isOpen = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
function close (value: boolean) {
|
||||
isOpen.value = value
|
||||
emit('close')
|
||||
}
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
isOpen,
|
||||
close
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,211 +1,191 @@
|
||||
<template>
|
||||
<transition appear v-bind="transitionClass">
|
||||
<transition appear v-bind="ui.transition">
|
||||
<div
|
||||
:class="['z-50 w-full pointer-events-auto', backgroundClass, roundedClass, shadowClass]"
|
||||
:class="[ui.wrapper, ui.background, ui.rounded, ui.shadow]"
|
||||
@mouseover="onMouseover"
|
||||
@mouseleave="onMouseleave"
|
||||
>
|
||||
<div :class="['relative overflow-hidden', roundedClass, ringClass]">
|
||||
<div :class="[ui.container, ui.rounded, ui.ring]">
|
||||
<div class="p-4">
|
||||
<div class="flex gap-3" :class="{ 'items-start': description, 'items-center': !description }">
|
||||
<div v-if="iconName" class="flex-shrink-0">
|
||||
<Icon :name="iconName" :class="iconClass" />
|
||||
</div>
|
||||
<Icon v-if="icon" :name="icon" :class="ui.icon" />
|
||||
<Avatar v-if="avatar" v-bind="avatar" :class="ui.avatar" />
|
||||
|
||||
<div class="w-0 flex-1">
|
||||
<p class="text-sm font-medium u-text-gray-900">
|
||||
<p :class="ui.title">
|
||||
{{ title }}
|
||||
</p>
|
||||
<p v-if="description" class="mt-1 text-sm leading-5 u-text-gray-500">
|
||||
<p v-if="description" :class="ui.description">
|
||||
{{ description }}
|
||||
</p>
|
||||
|
||||
<div v-if="description && actions.length" class="mt-3 flex items-center gap-6">
|
||||
<button v-for="(action, index) of actions" :key="index" type="button" class="text-sm font-medium focus:outline-none text-primary-500 dark:text-primary-400 hover:text-primary-400 dark:hover:text-primary-500" @click.stop="onAction(action)">
|
||||
{{ action.label }}
|
||||
</button>
|
||||
<div v-if="description && actions.length" class="mt-3 flex items-center gap-2">
|
||||
<Button v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.action, ...action }" @click.stop="onAction(action)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-shrink-0 flex items-center gap-3">
|
||||
<div v-if="!description && actions.length" class="flex items-center gap-2">
|
||||
<button v-for="(action, index) of actions" :key="index" type="button" class="text-sm font-medium focus:outline-none text-primary-500 dark:text-primary-400 hover:text-primary-400 dark:hover:text-primary-500" @click.stop="onAction(action)">
|
||||
{{ action.label }}
|
||||
</button>
|
||||
<Button v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.action, ...action }" @click.stop="onAction(action)" />
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="inline-flex transition duration-150 ease-in-out u-text-gray-400 focus:outline-none hover:u-text-gray-500 focus:u-text-gray-500"
|
||||
@click.stop="onClose"
|
||||
>
|
||||
<span class="sr-only">Close</span>
|
||||
<Icon :name="$ui.notification.close.icon.name" class="w-5 h-5" />
|
||||
</button>
|
||||
<Button v-if="close" v-bind="{ ...ui.default.close, ...close }" @click.stop="onClose" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="timeout" class="absolute bottom-0 left-0 right-0 h-1">
|
||||
<div class="h-1 bg-primary-500" :style="progressBarStyle" />
|
||||
</div>
|
||||
<div v-if="timeout" :class="ui.progress" :style="progressBarStyle" />
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watchEffect } from 'vue'
|
||||
<script lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watchEffect, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import Avatar from '../elements/Avatar.vue'
|
||||
import Button from '../elements/Button.vue'
|
||||
import { useTimer } from '../../composables/useTimer'
|
||||
import { classNames } from '../../utils'
|
||||
import type { ToastNotificationAction } from '../../types'
|
||||
import $ui from '#build/ui'
|
||||
import type { Avatar as AvatarType } from '../../types/avatar'
|
||||
import type { Button as ButtonType } from '../../types/button'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Icon,
|
||||
Avatar,
|
||||
// eslint-disable-next-line vue/no-reserved-component-names
|
||||
Button
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: null,
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.notification.type).includes(value)
|
||||
props: {
|
||||
id: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
avatar: {
|
||||
type: Object as PropType<Partial<AvatarType>>,
|
||||
default: null
|
||||
},
|
||||
close: {
|
||||
type: Object as PropType<Partial<ButtonType>>,
|
||||
default: () => appConfig.ui.notification.default.close
|
||||
},
|
||||
timeout: {
|
||||
type: Number,
|
||||
default: 5000
|
||||
},
|
||||
actions: {
|
||||
type: Array as PropType<ToastNotificationAction[]>,
|
||||
default: () => []
|
||||
},
|
||||
callback: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.notification>>,
|
||||
default: () => appConfig.ui.notification
|
||||
}
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
backgroundClass: {
|
||||
type: String,
|
||||
default: () => $ui.notification.background
|
||||
},
|
||||
shadowClass: {
|
||||
type: String,
|
||||
default: () => $ui.notification.shadow
|
||||
},
|
||||
ringClass: {
|
||||
type: String,
|
||||
default: () => $ui.notification.ring
|
||||
},
|
||||
roundedClass: {
|
||||
type: String,
|
||||
default: () => $ui.notification.rounded
|
||||
},
|
||||
transitionClass: {
|
||||
type: Object,
|
||||
default: () => $ui.notification.transition
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
iconBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.notification.icon.base
|
||||
},
|
||||
timeout: {
|
||||
type: Number,
|
||||
default: 5000
|
||||
},
|
||||
actions: {
|
||||
type: Array as PropType<{
|
||||
label: string,
|
||||
click: Function
|
||||
}[]>,
|
||||
default: () => []
|
||||
},
|
||||
callback: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
emits: ['close'],
|
||||
setup (props, { emit }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
const ui = computed<Partial<typeof appConfig.ui.notification>>(() => defu({}, props.ui, appConfig.ui.notification))
|
||||
|
||||
let timer: any = null
|
||||
const remaining = ref(props.timeout)
|
||||
let timer: any = null
|
||||
const remaining = ref(props.timeout)
|
||||
|
||||
const iconName = computed(() => {
|
||||
return props.icon || $ui.notification.type[props.type]
|
||||
})
|
||||
const progressBarStyle = computed(() => {
|
||||
const remainingPercent = remaining.value / props.timeout * 100
|
||||
|
||||
const iconClass = computed(() => {
|
||||
return classNames(
|
||||
props.iconBaseClass,
|
||||
$ui.notification.icon.color[props.type] || 'u-text-gray-400'
|
||||
)
|
||||
})
|
||||
return { width: `${remainingPercent || 0}%` }
|
||||
})
|
||||
|
||||
const progressBarStyle = computed(() => {
|
||||
const remainingPercent = remaining.value / props.timeout * 100
|
||||
return { width: `${remainingPercent || 0}%` }
|
||||
})
|
||||
function onMouseover () {
|
||||
if (timer) {
|
||||
timer.pause()
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseover () {
|
||||
if (timer) {
|
||||
timer.pause()
|
||||
}
|
||||
}
|
||||
function onMouseleave () {
|
||||
if (timer) {
|
||||
timer.resume()
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseleave () {
|
||||
if (timer) {
|
||||
timer.resume()
|
||||
}
|
||||
}
|
||||
function onClose () {
|
||||
if (timer) {
|
||||
timer.stop()
|
||||
}
|
||||
|
||||
function onClose () {
|
||||
if (timer) {
|
||||
timer.stop()
|
||||
}
|
||||
if (props.callback) {
|
||||
props.callback()
|
||||
}
|
||||
|
||||
if (props.callback) {
|
||||
props.callback()
|
||||
}
|
||||
emit('close')
|
||||
}
|
||||
|
||||
emit('close')
|
||||
}
|
||||
function onAction (action: ToastNotificationAction) {
|
||||
if (timer) {
|
||||
timer.stop()
|
||||
}
|
||||
|
||||
function onAction (action: ToastNotificationAction) {
|
||||
if (timer) {
|
||||
timer.stop()
|
||||
}
|
||||
if (action.click) {
|
||||
action.click()
|
||||
}
|
||||
|
||||
if (action.click) {
|
||||
action.click()
|
||||
}
|
||||
emit('close')
|
||||
}
|
||||
|
||||
emit('close')
|
||||
}
|
||||
onMounted(() => {
|
||||
if (!props.timeout) {
|
||||
return
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!props.timeout) {
|
||||
return
|
||||
}
|
||||
timer = useTimer(() => {
|
||||
onClose()
|
||||
}, props.timeout)
|
||||
|
||||
timer = useTimer(() => {
|
||||
onClose()
|
||||
}, props.timeout)
|
||||
watchEffect(() => {
|
||||
remaining.value = timer.remaining.value
|
||||
})
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
remaining.value = timer.remaining.value
|
||||
})
|
||||
})
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
timer.stop()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
timer.stop()
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
progressBarStyle,
|
||||
onMouseover,
|
||||
onMouseleave,
|
||||
onClose,
|
||||
onAction
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UNotification' }
|
||||
</script>
|
||||
|
||||
@@ -1,31 +1,57 @@
|
||||
<template>
|
||||
<div class="fixed bottom-0 right-0 flex flex-col justify-end w-full z-[55] sm:w-96">
|
||||
<div v-if="notifications.length" class="px-4 py-6 space-y-3 overflow-y-auto sm:px-6">
|
||||
<div
|
||||
v-for="notification of notifications"
|
||||
:key="notification.id"
|
||||
>
|
||||
<div :class="ui.wrapper">
|
||||
<div v-if="notifications.length" :class="ui.container">
|
||||
<div v-for="notification of notifications" :key="notification.id">
|
||||
<Notification
|
||||
v-bind="notification"
|
||||
:class="notification.click && 'cursor-pointer'"
|
||||
@click="notification.click && notification.click(notification)"
|
||||
@close="toast.removeNotification(notification.id)"
|
||||
@close="toast.remove(notification.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import type { ToastNotification } from '../../types'
|
||||
import { useToast } from '../../composables/useToast'
|
||||
import Notification from './Notification.vue'
|
||||
import { useState } from '#imports'
|
||||
import { useState, useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
const toast = useToast()
|
||||
const notifications = useState<ToastNotification[]>('notifications', () => [])
|
||||
</script>
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UNotifications' }
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Notification
|
||||
},
|
||||
props: {
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.notifications>>,
|
||||
default: () => appConfig.ui.notifications
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.notifications>>(() => defu({}, props.ui, appConfig.ui.notifications))
|
||||
|
||||
const toast = useToast()
|
||||
const notifications = useState<ToastNotification[]>('notifications', () => [])
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
toast,
|
||||
notifications
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Popover v-slot="{ open, close }" :class="wrapperClass" @mouseleave="onMouseLeave">
|
||||
<Popover v-slot="{ open, close }" :class="ui.wrapper" @mouseleave="onMouseLeave">
|
||||
<PopoverButton
|
||||
ref="trigger"
|
||||
as="div"
|
||||
@@ -15,9 +15,9 @@
|
||||
</slot>
|
||||
</PopoverButton>
|
||||
|
||||
<div v-if="open" ref="container" :class="[containerClass, widthClass]" @mouseover="onMouseOver">
|
||||
<transition appear v-bind="transitionClass">
|
||||
<PopoverPanel :class="[baseClass, ringClass, roundedClass, shadowClass, backgroundClass]" static>
|
||||
<div v-if="open" ref="container" :class="[ui.container, ui.width]" @mouseover="onMouseOver">
|
||||
<transition appear v-bind="ui.transition">
|
||||
<PopoverPanel :class="[ui.base, ui.background, ui.ring, ui.rounded, ui.shadow]" static>
|
||||
<slot name="panel" :open="open" :close="close" />
|
||||
</PopoverPanel>
|
||||
</transition>
|
||||
@@ -25,140 +25,131 @@
|
||||
</Popover>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
<script lang="ts">
|
||||
import { computed, ref, onMounted, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
|
||||
import { usePopper } from '../../composables/usePopper'
|
||||
import type { PopperOptions } from '../../types'
|
||||
import $ui from '#build/ui'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
const props = defineProps({
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'click',
|
||||
validator: (value: string) => {
|
||||
return ['click', 'hover'].includes(value)
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Popover,
|
||||
PopoverButton,
|
||||
PopoverPanel
|
||||
},
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'click',
|
||||
validator: (value: string) => {
|
||||
return ['click', 'hover'].includes(value)
|
||||
}
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
openDelay: {
|
||||
type: Number,
|
||||
default: 50
|
||||
},
|
||||
closeDelay: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
popper: {
|
||||
type: Object as PropType<PopperOptions>,
|
||||
default: () => ({})
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.popover>>,
|
||||
default: () => appConfig.ui.popover
|
||||
}
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.popover.wrapper
|
||||
},
|
||||
containerClass: {
|
||||
type: String,
|
||||
default: () => $ui.popover.container
|
||||
},
|
||||
widthClass: {
|
||||
type: String,
|
||||
default: () => $ui.popover.width
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.popover.base
|
||||
},
|
||||
backgroundClass: {
|
||||
type: String,
|
||||
default: () => $ui.popover.background
|
||||
},
|
||||
shadowClass: {
|
||||
type: String,
|
||||
default: () => $ui.popover.shadow
|
||||
},
|
||||
roundedClass: {
|
||||
type: String,
|
||||
default: () => $ui.popover.rounded
|
||||
},
|
||||
ringClass: {
|
||||
type: String,
|
||||
default: () => $ui.popover.ring
|
||||
},
|
||||
transitionClass: {
|
||||
type: Object,
|
||||
default: () => $ui.popover.transition
|
||||
},
|
||||
popperOptions: {
|
||||
type: Object as PropType<PopperOptions>,
|
||||
default: () => ({})
|
||||
},
|
||||
openDelay: {
|
||||
type: Number,
|
||||
default: 50
|
||||
},
|
||||
closeDelay: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
setup (props) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const popperOptions = computed<PopperOptions>(() => defu({}, props.popperOptions, $ui.popover.popperOptions))
|
||||
const ui = computed<Partial<typeof appConfig.ui.popover>>(() => defu({}, props.ui, appConfig.ui.popover))
|
||||
|
||||
const [trigger, container] = usePopper(popperOptions.value)
|
||||
const popper = computed<PopperOptions>(() => defu({}, props.popper, ui.value.popper as PopperOptions))
|
||||
|
||||
// https://github.com/tailwindlabs/headlessui/blob/f66f4926c489fc15289d528294c23a3dc2aee7b1/packages/%40headlessui-vue/src/components/popover/popover.ts#L151
|
||||
const popoverApi = ref<any>(null)
|
||||
const [trigger, container] = usePopper(popper.value)
|
||||
|
||||
let openTimeout: NodeJS.Timeout | null = null
|
||||
let closeTimeout: NodeJS.Timeout | null = null
|
||||
// https://github.com/tailwindlabs/headlessui/blob/f66f4926c489fc15289d528294c23a3dc2aee7b1/packages/%40headlessui-vue/src/components/popover/popover.ts#L151
|
||||
const popoverApi = ref<any>(null)
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
// @ts-expect-error internals
|
||||
const popoverProvides = trigger.value?.$.provides
|
||||
if (!popoverProvides) {
|
||||
return
|
||||
let openTimeout: NodeJS.Timeout | null = null
|
||||
let closeTimeout: NodeJS.Timeout | null = null
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
// @ts-expect-error internals
|
||||
const popoverProvides = trigger.value?.$.provides
|
||||
if (!popoverProvides) {
|
||||
return
|
||||
}
|
||||
const popoverProvidesSymbols = Object.getOwnPropertySymbols(popoverProvides)
|
||||
popoverApi.value = popoverProvidesSymbols.length && popoverProvides[popoverProvidesSymbols[0]]
|
||||
}, 200)
|
||||
})
|
||||
|
||||
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
|
||||
}, props.openDelay)
|
||||
}
|
||||
const popoverProvidesSymbols = Object.getOwnPropertySymbols(popoverProvides)
|
||||
popoverApi.value = popoverProvidesSymbols.length && popoverProvides[popoverProvidesSymbols[0]]
|
||||
}, 200)
|
||||
|
||||
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
|
||||
}, props.closeDelay)
|
||||
}
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
trigger,
|
||||
container,
|
||||
onMouseOver,
|
||||
onMouseLeave
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
}, props.openDelay)
|
||||
}
|
||||
|
||||
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
|
||||
}, props.closeDelay)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UPopover' }
|
||||
</script>
|
||||
|
||||
@@ -1,24 +1,12 @@
|
||||
<template>
|
||||
<TransitionRoot as="template" :appear="appear" :show="isOpen">
|
||||
<Dialog :class="[wrapperClass, { 'justify-end': side === 'right' }]" @close="close">
|
||||
<TransitionChild
|
||||
v-if="overlay"
|
||||
as="template"
|
||||
:appear="appear"
|
||||
v-bind="overlayTransition"
|
||||
>
|
||||
<div class="fixed inset-0 transition-opacity" :class="overlayBackgroundClass" />
|
||||
<Dialog :class="[ui.wrapper, { 'justify-end': side === 'right' }]" @close="close">
|
||||
<TransitionChild v-if="overlay" as="template" :appear="appear" v-bind="ui.overlay.transition">
|
||||
<div :class="[ui.overlay.base, ui.overlay.background]" />
|
||||
</TransitionChild>
|
||||
|
||||
<TransitionChild
|
||||
as="template"
|
||||
:appear="appear"
|
||||
v-bind="slideoverTransition"
|
||||
>
|
||||
<DialogPanel :class="slideoverClass">
|
||||
<div v-if="$slots.header" :class="headerClass">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
<TransitionChild as="template" :appear="appear" v-bind="transitionClass">
|
||||
<DialogPanel :class="[ui.base, ui.width, ui.background, ui.ring]">
|
||||
<slot />
|
||||
</DialogPanel>
|
||||
</TransitionChild>
|
||||
@@ -26,116 +14,95 @@
|
||||
</TransitionRoot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { WritableComputedRef, PropType } from 'vue'
|
||||
import { Dialog, DialogPanel, TransitionRoot, TransitionChild } from '@headlessui/vue'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false
|
||||
},
|
||||
appear: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
side: {
|
||||
type: String,
|
||||
default: 'left',
|
||||
validator: (value: string) => ['left', 'right'].includes(value)
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.slideover.wrapper
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.slideover.base
|
||||
},
|
||||
backgroundClass: {
|
||||
type: String,
|
||||
default: () => $ui.slideover.background
|
||||
},
|
||||
overlay: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
overlayBackgroundClass: {
|
||||
type: String,
|
||||
default: () => $ui.slideover.overlay.background
|
||||
},
|
||||
overlayTransitionClass: {
|
||||
type: Object,
|
||||
default: () => $ui.slideover.overlay.transition
|
||||
},
|
||||
widthClass: {
|
||||
type: String,
|
||||
default: () => $ui.slideover.width
|
||||
},
|
||||
headerClass: {
|
||||
type: String,
|
||||
default: () => $ui.slideover.header
|
||||
},
|
||||
transition: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
transitionClass: {
|
||||
type: Object,
|
||||
default: () => $ui.slideover.transition
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'close'])
|
||||
|
||||
const isOpen: WritableComputedRef<boolean> = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
const slideoverClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
props.widthClass,
|
||||
props.backgroundClass
|
||||
)
|
||||
})
|
||||
|
||||
const overlayTransition = computed(() => {
|
||||
if (!props.transition) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return props.overlayTransitionClass
|
||||
})
|
||||
|
||||
const slideoverTransition = computed(() => {
|
||||
if (!props.transition) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return {
|
||||
enterFrom: props.side === 'left' ? '-translate-x-full' : 'translate-x-full',
|
||||
enterTo: 'translate-x-0',
|
||||
leaveFrom: 'translate-x-0',
|
||||
leaveTo: props.side === 'left' ? '-translate-x-full' : 'translate-x-full',
|
||||
...props.transitionClass
|
||||
}
|
||||
})
|
||||
|
||||
function close (value: boolean) {
|
||||
isOpen.value = value
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'USlideover' }
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import type { WritableComputedRef, PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { Dialog, DialogPanel, TransitionRoot, TransitionChild } from '@headlessui/vue'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
// eslint-disable-next-line vue/no-reserved-component-names
|
||||
Dialog,
|
||||
DialogPanel,
|
||||
TransitionRoot,
|
||||
TransitionChild
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false
|
||||
},
|
||||
appear: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
side: {
|
||||
type: String,
|
||||
default: 'left',
|
||||
validator: (value: string) => ['left', 'right'].includes(value)
|
||||
},
|
||||
overlay: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
transition: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.slideover>>,
|
||||
default: () => appConfig.ui.slideover
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'close'],
|
||||
setup (props, { emit }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.slideover>>(() => defu({}, props.ui, appConfig.ui.slideover))
|
||||
|
||||
const isOpen: WritableComputedRef<boolean> = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
const transitionClass = computed(() => {
|
||||
if (!props.transition) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return {
|
||||
...ui.value.transition,
|
||||
enterFrom: props.side === 'left' ? '-translate-x-full' : 'translate-x-full',
|
||||
enterTo: 'translate-x-0',
|
||||
leaveFrom: 'translate-x-0',
|
||||
leaveTo: props.side === 'left' ? '-translate-x-full' : 'translate-x-full'
|
||||
}
|
||||
})
|
||||
|
||||
function close (value: boolean) {
|
||||
isOpen.value = value
|
||||
emit('close')
|
||||
}
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
isOpen,
|
||||
transitionClass,
|
||||
close
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<template>
|
||||
<div ref="trigger" :class="wrapperClass" @mouseover="onMouseOver" @mouseleave="onMouseLeave">
|
||||
<div ref="trigger" :class="ui.wrapper" @mouseover="onMouseOver" @mouseleave="onMouseLeave">
|
||||
<slot :open="open">
|
||||
Hover me
|
||||
hover
|
||||
</slot>
|
||||
|
||||
<div v-if="open && !prevent" ref="container" :class="[containerClass, widthClass]">
|
||||
<transition appear v-bind="transitionClass">
|
||||
<div :class="[baseClass, backgroundClass, roundedClass, shadowClass, ringClass]">
|
||||
<div v-if="open && !prevent" ref="container" :class="[ui.container, ui.width]">
|
||||
<transition appear v-bind="ui.transition">
|
||||
<div :class="[ui.base, ui.background, ui.rounded, ui.shadow, ui.ring]">
|
||||
<slot name="text">
|
||||
{{ text }}
|
||||
</slot>
|
||||
|
||||
<span v-if="shortcuts?.length" :class="shortcutsClass">
|
||||
<span class="mr-1 u-text-gray-700">·</span>
|
||||
<kbd v-for="shortcut of shortcuts" :key="shortcut" class="flex items-center justify-center font-sans px-1 h-4 min-w-[16px] text-[10px] u-bg-gray-100 rounded u-text-gray-900">
|
||||
<span v-if="shortcuts?.length" :class="ui.shortcuts">
|
||||
<span class="mr-1 text-gray-700 dark:text-gray-200">·</span>
|
||||
<kbd v-for="shortcut of shortcuts" :key="shortcut" class="flex items-center justify-center font-sans px-1 h-4 min-w-[16px] text-[10px] bg-gray-100 dark:bg-gray-800 rounded text-gray-900 dark:text-white">
|
||||
{{ shortcut }}
|
||||
</kbd>
|
||||
</span>
|
||||
@@ -23,125 +23,108 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts">
|
||||
import { computed, ref, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { usePopper } from '../../composables/usePopper'
|
||||
import type { PopperOptions } from '../../types'
|
||||
import $ui from '#build/ui'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
const props = defineProps({
|
||||
text: {
|
||||
type: String,
|
||||
default: null
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
text: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
prevent: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
shortcuts: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => []
|
||||
},
|
||||
openDelay: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
closeDelay: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
popper: {
|
||||
type: Object as PropType<PopperOptions>,
|
||||
default: () => ({})
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.tooltip>>,
|
||||
default: () => appConfig.ui.tooltip
|
||||
}
|
||||
},
|
||||
prevent: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
shortcuts: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => []
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.tooltip.wrapper
|
||||
},
|
||||
containerClass: {
|
||||
type: String,
|
||||
default: () => $ui.tooltip.container
|
||||
},
|
||||
widthClass: {
|
||||
type: String,
|
||||
default: () => $ui.tooltip.width
|
||||
},
|
||||
backgroundClass: {
|
||||
type: String,
|
||||
default: () => $ui.tooltip.background
|
||||
},
|
||||
shadowClass: {
|
||||
type: String,
|
||||
default: () => $ui.tooltip.shadow
|
||||
},
|
||||
ringClass: {
|
||||
type: String,
|
||||
default: () => $ui.tooltip.ring
|
||||
},
|
||||
roundedClass: {
|
||||
type: String,
|
||||
default: () => $ui.tooltip.rounded
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.tooltip.base
|
||||
},
|
||||
transitionClass: {
|
||||
type: Object,
|
||||
default: () => $ui.tooltip.transition
|
||||
},
|
||||
popperOptions: {
|
||||
type: Object as PropType<PopperOptions>,
|
||||
default: () => ({})
|
||||
},
|
||||
shortcutsClass: {
|
||||
type: String,
|
||||
default: () => $ui.tooltip.shortcuts
|
||||
},
|
||||
openDelay: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
closeDelay: {
|
||||
type: Number,
|
||||
default: 0
|
||||
setup (props) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.tooltip>>(() => defu({}, props.ui, appConfig.ui.tooltip))
|
||||
|
||||
const popper = computed<PopperOptions>(() => defu({}, props.popper, ui.value.popper as PopperOptions))
|
||||
|
||||
const [trigger, container] = usePopper(popper.value)
|
||||
|
||||
const open = ref(false)
|
||||
|
||||
let openTimeout: NodeJS.Timeout | null = null
|
||||
let closeTimeout: NodeJS.Timeout | null = null
|
||||
|
||||
// Methods
|
||||
|
||||
function onMouseOver () {
|
||||
// cancel programmed closing
|
||||
if (closeTimeout) {
|
||||
clearTimeout(closeTimeout)
|
||||
closeTimeout = null
|
||||
}
|
||||
// dropdown already open
|
||||
if (open.value) {
|
||||
return
|
||||
}
|
||||
openTimeout = openTimeout || setTimeout(() => {
|
||||
open.value = true
|
||||
openTimeout = null
|
||||
}, props.openDelay)
|
||||
}
|
||||
|
||||
function onMouseLeave () {
|
||||
// cancel programmed opening
|
||||
if (openTimeout) {
|
||||
clearTimeout(openTimeout)
|
||||
openTimeout = null
|
||||
}
|
||||
// dropdown already closed
|
||||
if (!open.value) {
|
||||
return
|
||||
}
|
||||
closeTimeout = closeTimeout || setTimeout(() => {
|
||||
open.value = false
|
||||
closeTimeout = null
|
||||
}, props.closeDelay)
|
||||
}
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
trigger,
|
||||
container,
|
||||
open,
|
||||
onMouseOver,
|
||||
onMouseLeave
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const popperOptions = computed<PopperOptions>(() => defu({}, props.popperOptions, $ui.tooltip.popperOptions))
|
||||
|
||||
const [trigger, container] = usePopper(popperOptions.value)
|
||||
|
||||
const open = ref(false)
|
||||
|
||||
let openTimeout: NodeJS.Timeout | null = null
|
||||
let closeTimeout: NodeJS.Timeout | null = null
|
||||
|
||||
// Methods
|
||||
|
||||
function onMouseOver () {
|
||||
// cancel programmed closing
|
||||
if (closeTimeout) {
|
||||
clearTimeout(closeTimeout)
|
||||
closeTimeout = null
|
||||
}
|
||||
// dropdown already open
|
||||
if (open.value) {
|
||||
return
|
||||
}
|
||||
openTimeout = openTimeout || setTimeout(() => {
|
||||
open.value = true
|
||||
openTimeout = null
|
||||
}, props.openDelay)
|
||||
}
|
||||
|
||||
function onMouseLeave () {
|
||||
// cancel programmed opening
|
||||
if (openTimeout) {
|
||||
clearTimeout(openTimeout)
|
||||
openTimeout = null
|
||||
}
|
||||
// dropdown already closed
|
||||
if (!open.value) {
|
||||
return
|
||||
}
|
||||
closeTimeout = closeTimeout || setTimeout(() => {
|
||||
open.value = false
|
||||
closeTimeout = null
|
||||
}, props.closeDelay)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UTooltip' }
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import type { ToastNotification } from '../types/toast'
|
||||
import { useToast } from './useToast'
|
||||
|
||||
export function useCopyToClipboard () {
|
||||
export function useCopyToClipboard (options: Partial<ToastNotification> = {}) {
|
||||
const { copy: copyToClipboard, isSupported } = useClipboard()
|
||||
const toast = useToast()
|
||||
|
||||
@@ -15,11 +16,12 @@ export function useCopyToClipboard () {
|
||||
return
|
||||
}
|
||||
|
||||
toast.success(success)
|
||||
toast.add({ ...success, ...options })
|
||||
}, function (e) {
|
||||
toast.error({
|
||||
toast.add({
|
||||
...failure,
|
||||
description: failure.description || e.message
|
||||
description: failure.description || e.message,
|
||||
...options
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState } from '#imports'
|
||||
export function useToast () {
|
||||
const notifications = useState<ToastNotification[]>('notifications', () => [])
|
||||
|
||||
function addNotification (notification: Partial<ToastNotification>) {
|
||||
function add (notification: Partial<ToastNotification>) {
|
||||
const body = {
|
||||
id: new Date().getTime().toString(),
|
||||
...notification
|
||||
@@ -18,24 +18,12 @@ export function useToast () {
|
||||
return body
|
||||
}
|
||||
|
||||
function removeNotification (id: string) {
|
||||
function remove (id: string) {
|
||||
notifications.value = notifications.value.filter((n: ToastNotification) => n.id !== id)
|
||||
}
|
||||
|
||||
const success = (notification: Partial<ToastNotification> = {}) => addNotification({ type: 'success', ...notification })
|
||||
|
||||
const info = (notification: Partial<ToastNotification> = {}) => addNotification({ type: 'info', ...notification })
|
||||
|
||||
const warning = (notification: Partial<ToastNotification> = {}) => addNotification({ type: 'warning', ...notification })
|
||||
|
||||
const error = (notification: Partial<ToastNotification>) => addNotification({ type: 'error', title: 'An error occurred!', ...notification })
|
||||
|
||||
return {
|
||||
addNotification,
|
||||
removeNotification,
|
||||
success,
|
||||
info,
|
||||
warning,
|
||||
error
|
||||
add,
|
||||
remove
|
||||
}
|
||||
}
|
||||
|
||||
34
src/runtime/plugins/colors.ts
Normal file
34
src/runtime/plugins/colors.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { computed } from 'vue'
|
||||
import { defineNuxtPlugin, useHead, useAppConfig } from '#imports'
|
||||
import colors from '#tailwind-config/theme/colors'
|
||||
|
||||
function hexToRgb (hex) {
|
||||
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
|
||||
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i
|
||||
hex = hex.replace(shorthandRegex, function (_, r, g, b) {
|
||||
return r + r + g + g + b + b
|
||||
})
|
||||
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
return result
|
||||
? `${parseInt(result[1], 16)} ${parseInt(result[2], 16)} ${parseInt(result[3], 16)}`
|
||||
: null
|
||||
}
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const root = computed(() => `:root {
|
||||
${Object.entries(colors[appConfig.ui.primary]).map(([key, value]) => `--color-primary-${key}: ${hexToRgb(value)};`).join('\n')}
|
||||
|
||||
${Object.entries(colors[appConfig.ui.gray]).map(([key, value]) => `--color-gray-${key}: ${hexToRgb(value)};`).join('\n')}
|
||||
}`)
|
||||
|
||||
// Head
|
||||
|
||||
useHead({
|
||||
style: [{
|
||||
innerHTML: () => root.value
|
||||
}]
|
||||
})
|
||||
})
|
||||
@@ -1,650 +0,0 @@
|
||||
export default function defaultPreset (variantColors: string[]) {
|
||||
const button = {
|
||||
base: 'font-medium focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 focus:ring-offset-white dark:focus:ring-offset-black',
|
||||
rounded: 'rounded-md',
|
||||
size: {
|
||||
xxs: 'text-xs',
|
||||
xs: 'text-xs',
|
||||
sm: 'text-sm leading-4',
|
||||
md: 'text-sm',
|
||||
lg: 'text-base',
|
||||
xl: 'text-base'
|
||||
},
|
||||
spacing: {
|
||||
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'
|
||||
},
|
||||
square: {
|
||||
xxs: 'p-1',
|
||||
xs: 'p-1.5',
|
||||
sm: 'p-2',
|
||||
md: 'p-2',
|
||||
lg: 'p-2',
|
||||
xl: 'p-3'
|
||||
},
|
||||
compact: {
|
||||
xxs: 'p-1 sm:px-2',
|
||||
xs: 'p-1.5 sm:px-2.5',
|
||||
sm: 'p-2 sm:px-3',
|
||||
md: 'p-2 sm:px-4',
|
||||
lg: 'p-2 sm:px-4',
|
||||
xl: 'p-3 sm:px-6'
|
||||
},
|
||||
variant: {
|
||||
...variantColors.reduce((acc: any, color: string) => {
|
||||
acc[color] = `shadow-sm border border-transparent text-white bg-${color}-600 hover:bg-${color}-700 disabled:bg-${color}-600 focus:ring-2 focus:ring-offset-2 focus:ring-${color}-500`
|
||||
return acc
|
||||
}, {}),
|
||||
secondary: 'border border-transparent text-primary-700 bg-primary-100 hover:bg-primary-200 disabled:bg-primary-100 focus:ring-2 focus:ring-offset-2 focus:ring-primary-500',
|
||||
white: 'shadow-sm border u-border-gray-300 u-text-gray-700 u-bg-white hover:u-bg-gray-50 disabled:u-bg-white focus:ring-2 focus:ring-offset-2 focus:ring-primary-500',
|
||||
gray: 'shadow-sm border u-border-gray-300 u-text-gray-700 u-bg-gray-50 hover:u-bg-gray-100 disabled:u-bg-gray-50 focus:ring-2 focus:ring-offset-2 focus:ring-primary-500',
|
||||
black: 'shadow-sm border border-transparent u-text-white u-bg-gray-800 hover:u-bg-gray-900 disabled:u-bg-gray-800 focus:ring-2 focus:ring-offset-2 focus:ring-primary-500',
|
||||
transparent: 'border border-transparent u-text-gray-500 hover:u-text-gray-700 focus:u-text-gray-700 disabled:hover:u-text-gray-500',
|
||||
link: 'border border-transparent text-primary-500 hover:text-primary-700 focus:text-primary-700'
|
||||
},
|
||||
icon: {
|
||||
base: 'flex-shrink-0',
|
||||
loading: 'i-heroicons-arrow-path',
|
||||
size: {
|
||||
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'
|
||||
},
|
||||
leading: {
|
||||
spacing: {
|
||||
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'
|
||||
},
|
||||
compactSpacing: {
|
||||
xxs: 'sm:-ml-0.5 sm:mr-1',
|
||||
xs: 'sm:-ml-0.5 sm:mr-1.5',
|
||||
sm: 'sm:-ml-0.5 sm:mr-2',
|
||||
md: 'sm:-ml-1 sm:mr-2',
|
||||
lg: 'sm:-ml-1 sm:mr-3',
|
||||
xl: 'sm:-ml-1 sm:mr-3'
|
||||
}
|
||||
},
|
||||
trailing: {
|
||||
spacing: {
|
||||
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'
|
||||
},
|
||||
compactSpacing: {
|
||||
xxs: 'sm:ml-1 sm:-mr-0.5',
|
||||
xs: 'sm:ml-1.5 sm:-mr-0.5',
|
||||
sm: 'sm:ml-2 sm:-mr-0.5',
|
||||
md: 'sm:ml-2 sm:-mr-1',
|
||||
lg: 'sm:ml-3 sm:-mr-1',
|
||||
xl: 'sm:ml-3 sm:-mr-1'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const badge = {
|
||||
base: 'inline-flex items-center font-medium',
|
||||
size: {
|
||||
sm: 'text-xs px-2 py-0.5',
|
||||
md: 'text-sm px-2.5 py-0.5',
|
||||
lg: 'text-sm px-3 py-0.5',
|
||||
xl: 'text-sm px-4 py-1'
|
||||
},
|
||||
variant: {
|
||||
...variantColors.reduce((acc: any, color: string) => {
|
||||
acc[color] = `bg-${color}-100 dark:bg-${color}-700 text-${color}-800 dark:text-${color}-100`
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
}
|
||||
|
||||
const formGroup = {
|
||||
wrapper: '',
|
||||
label: 'block text-sm font-medium u-text-gray-700',
|
||||
labelWrapper: 'flex content-center justify-between',
|
||||
container: 'mt-1 relative',
|
||||
required: 'text-red-400',
|
||||
description: 'text-sm leading-5 u-text-gray-500',
|
||||
hint: 'text-sm leading-5 u-text-gray-500',
|
||||
help: 'mt-2 text-sm u-text-gray-500'
|
||||
}
|
||||
|
||||
const input = {
|
||||
wrapper: 'relative',
|
||||
base: 'relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none',
|
||||
size: {
|
||||
xxs: 'text-xs',
|
||||
xs: 'text-xs',
|
||||
sm: 'text-sm leading-4',
|
||||
md: 'text-sm',
|
||||
lg: 'text-base',
|
||||
xl: 'text-base'
|
||||
},
|
||||
spacing: {
|
||||
xxs: 'px-1 py-0.5',
|
||||
xs: 'px-2.5 py-1.5',
|
||||
sm: 'px-3 py-2',
|
||||
md: 'px-4 py-2',
|
||||
lg: 'px-4 py-2',
|
||||
xl: 'px-6 py-3'
|
||||
},
|
||||
leading: {
|
||||
spacing: {
|
||||
xxs: 'pl-7',
|
||||
xs: 'pl-7',
|
||||
sm: 'pl-10',
|
||||
md: 'pl-10',
|
||||
lg: 'pl-10',
|
||||
xl: 'pl-10'
|
||||
}
|
||||
},
|
||||
trailing: {
|
||||
spacing: {
|
||||
xxs: 'pr-7',
|
||||
xs: 'pr-7',
|
||||
sm: 'pr-10',
|
||||
md: 'pr-10',
|
||||
lg: 'pr-10',
|
||||
xl: 'pr-10'
|
||||
}
|
||||
},
|
||||
appearance: {
|
||||
default: 'u-bg-white u-text-gray-700 focus:ring-1 focus:ring-primary-500 focus:border-primary-500 dark:focus:border-primary-500 border u-border-gray-300 rounded-md shadow-sm',
|
||||
none: 'border-0 bg-transparent focus:ring-0 focus:shadow-none'
|
||||
},
|
||||
icon: {
|
||||
base: 'u-text-gray-400',
|
||||
loading: 'i-heroicons-arrow-path',
|
||||
size: {
|
||||
xxs: 'h-3 w-3',
|
||||
xs: 'h-4 w-4',
|
||||
sm: 'h-5 w-5',
|
||||
md: 'h-5 w-5',
|
||||
lg: 'h-5 w-5',
|
||||
xl: 'h-5 w-5'
|
||||
},
|
||||
leading: {
|
||||
wrapper: 'absolute inset-y-0 left-0 flex items-center pointer-events-none',
|
||||
spacing: {
|
||||
xxs: 'ml-2',
|
||||
xs: 'ml-2',
|
||||
sm: 'ml-2',
|
||||
md: 'ml-3',
|
||||
lg: 'ml-3',
|
||||
xl: 'ml-3'
|
||||
}
|
||||
},
|
||||
trailing: {
|
||||
wrapper: 'absolute inset-y-0 right-0 flex items-center pointer-events-none',
|
||||
spacing: {
|
||||
xxs: 'mr-2',
|
||||
xs: 'mr-2',
|
||||
sm: 'mr-2',
|
||||
md: 'mr-3',
|
||||
lg: 'mr-3',
|
||||
xl: 'mr-3'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const textarea = {
|
||||
...input
|
||||
}
|
||||
|
||||
const select = {
|
||||
...input
|
||||
}
|
||||
|
||||
const selectCustom = {
|
||||
...select,
|
||||
wrapper: 'relative',
|
||||
base: `${select.base} text-left cursor-default`,
|
||||
icon: {
|
||||
name: 'i-heroicons-chevron-up-down-20-solid',
|
||||
...select.icon
|
||||
},
|
||||
list: {
|
||||
container: 'z-20',
|
||||
width: 'w-full',
|
||||
base: 'u-bg-white shadow-lg rounded-md ring-1 u-ring-gray-200 focus:outline-none overflow-y-auto py-1 max-h-60',
|
||||
input: 'relative block w-full focus:ring-transparent text-sm px-4 py-2 u-text-gray-700 border-l-0 u-bg-white border-t-0 border-r-0 u-border-gray-200 focus:u-border-gray-200',
|
||||
option: {
|
||||
base: 'cursor-default select-none relative py-2 text-sm group',
|
||||
container: 'flex items-center gap-3',
|
||||
active: 'text-white bg-primary-600',
|
||||
inactive: 'u-text-gray-900',
|
||||
selected: 'font-semibold pl-4 pr-10',
|
||||
unselected: 'font-normal px-4',
|
||||
disabled: 'cursor-not-allowed opacity-50',
|
||||
empty: 'text-sm u-text-gray-400 px-4 py-2',
|
||||
icon: {
|
||||
name: 'i-heroicons-check-20-solid',
|
||||
base: 'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||
active: 'text-white',
|
||||
inactive: 'text-primary-600',
|
||||
size: 'h-5 w-5'
|
||||
}
|
||||
},
|
||||
transition: {
|
||||
leaveActiveClass: 'transition ease-in duration-100',
|
||||
leaveFromClass: 'opacity-100',
|
||||
leaveToClass: 'opacity-0'
|
||||
}
|
||||
},
|
||||
popperOptions: {
|
||||
placement: 'bottom-end'
|
||||
}
|
||||
}
|
||||
|
||||
const radio = {
|
||||
wrapper: 'relative flex items-start',
|
||||
base: 'h-4 w-4 text-primary-600 focus:ring-2 focus:ring-offset-2 u-bg-white dark:checked:bg-current dark:checked:border-transparent focus:ring-primary-500 focus:ring-offset-white dark:focus:ring-offset-black u-border-gray-300 disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
label: 'font-medium u-text-gray-700',
|
||||
required: 'text-red-400',
|
||||
help: 'u-text-gray-500'
|
||||
}
|
||||
|
||||
const checkbox = {
|
||||
...radio,
|
||||
base: `${radio.base} rounded`
|
||||
}
|
||||
|
||||
const card = {
|
||||
base: 'overflow-hidden',
|
||||
background: 'u-bg-white',
|
||||
border: 'u-border-gray-200',
|
||||
ring: 'ring-1 u-ring-gray-200',
|
||||
rounded: 'rounded-md',
|
||||
shadow: 'shadow',
|
||||
body: 'px-4 py-5 sm:p-6',
|
||||
header: 'px-4 py-5 sm:px-6',
|
||||
footer: 'px-4 py-4 sm:px-6'
|
||||
}
|
||||
|
||||
const modal = {
|
||||
wrapper: 'relative z-50',
|
||||
inner: 'fixed inset-0 overflow-y-auto',
|
||||
container: 'flex min-h-full items-end sm:items-center justify-center p-4 sm:p-0 text-center',
|
||||
base: 'relative inline-block align-bottom text-left overflow-hidden transform transition-all sm:my-8 sm:align-middle w-full',
|
||||
background: 'u-bg-white',
|
||||
overlay: {
|
||||
background: 'bg-gray-500/75 dark:bg-gray-600/75',
|
||||
transition: {
|
||||
enter: 'ease-out duration-300',
|
||||
enterFrom: 'opacity-0',
|
||||
enterTo: 'opacity-100',
|
||||
leave: 'ease-in duration-200',
|
||||
leaveFrom: 'opacity-100',
|
||||
leaveTo: 'opacity-0'
|
||||
}
|
||||
},
|
||||
border: '',
|
||||
ring: '',
|
||||
rounded: 'rounded-lg',
|
||||
shadow: 'shadow-xl',
|
||||
width: 'sm:max-w-lg',
|
||||
transition: {
|
||||
enter: 'ease-out duration-300',
|
||||
enterFrom: 'opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95',
|
||||
enterTo: 'opacity-100 translate-y-0 sm:scale-100',
|
||||
leave: 'ease-in duration-200',
|
||||
leaveFrom: 'opacity-100 translate-y-0 sm:scale-100',
|
||||
leaveTo: 'opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95'
|
||||
}
|
||||
}
|
||||
|
||||
const container = {
|
||||
constrained: 'max-w-7xl'
|
||||
}
|
||||
|
||||
const toggle = {
|
||||
base: '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-offset-2 focus:ring-primary-500 focus:ring-offset-white dark:focus:ring-offset-black',
|
||||
active: 'bg-primary-600',
|
||||
inactive: 'u-bg-gray-200',
|
||||
container: {
|
||||
base: 'pointer-events-none relative inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200',
|
||||
active: 'translate-x-5',
|
||||
inactive: 'translate-x-0'
|
||||
},
|
||||
icon: {
|
||||
base: 'absolute inset-0 h-full w-full flex items-center justify-center transition-opacity',
|
||||
active: 'opacity-100 ease-in duration-200',
|
||||
inactive: 'opacity-0 ease-out duration-100',
|
||||
on: 'h-3 w-3 text-primary-600',
|
||||
off: 'h-3 w-3 u-text-gray-400'
|
||||
}
|
||||
}
|
||||
|
||||
const verticalNavigation = {
|
||||
wrapper: 'space-y-1',
|
||||
base: 'group flex items-center text-sm font-medium rounded-md w-full relative',
|
||||
spacing: 'px-3 py-2',
|
||||
active: 'u-text-gray-900 u-bg-gray-100',
|
||||
inactive: 'u-text-gray-600 hover:u-text-gray-900 hover:u-bg-gray-50 focus:u-bg-gray-50',
|
||||
icon: {
|
||||
base: 'flex-shrink-0 h-6 w-6',
|
||||
spacing: '-ml-1 mr-3',
|
||||
active: 'u-text-gray-500',
|
||||
inactive: 'u-text-gray-400 group-hover:u-text-gray-500'
|
||||
},
|
||||
avatar: {
|
||||
base: 'flex-shrink-0',
|
||||
spacing: '-ml-1 mr-3'
|
||||
},
|
||||
badge: {
|
||||
base: 'ml-auto inline-block py-0.5 px-3 text-xs rounded-full',
|
||||
active: 'u-bg-white',
|
||||
inactive: 'u-bg-gray-100 u-text-gray-600 group-hover:u-bg-gray-200'
|
||||
}
|
||||
}
|
||||
|
||||
const alertDialog = {
|
||||
icon: {
|
||||
wrapper: 'mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-primary-100 sm:mx-0 sm:h-10 sm:w-10',
|
||||
base: 'h-6 w-6 text-primary-600'
|
||||
},
|
||||
title: 'text-lg leading-6 font-medium u-text-gray-900',
|
||||
description: 'text-sm u-text-gray-500'
|
||||
}
|
||||
|
||||
const dropdown = {
|
||||
wrapper: 'relative inline-flex text-left',
|
||||
container: 'z-20',
|
||||
width: 'w-48',
|
||||
background: 'u-bg-white',
|
||||
shadow: 'shadow-lg',
|
||||
rounded: 'rounded-md',
|
||||
ring: 'ring-1 u-ring-gray-200',
|
||||
base: 'focus:outline-none',
|
||||
divide: 'divide-y u-divide-gray-100',
|
||||
group: 'py-1',
|
||||
item: {
|
||||
base: 'group flex items-center gap-3 px-4 py-2 text-sm w-full',
|
||||
active: 'u-bg-gray-100 u-text-gray-900',
|
||||
inactive: 'u-text-gray-700',
|
||||
disabled: 'cursor-not-allowed opacity-50',
|
||||
icon: 'h-5 w-5 u-text-gray-400 group-hover:u-text-gray-500 flex-shrink-0',
|
||||
avatar: '-m-0.5 group-hover:u-bg-gray-200 flex-shrink-0',
|
||||
shortcuts: 'hidden md:inline-flex flex-shrink-0 text-xs font-semibold u-text-gray-500 ml-auto'
|
||||
},
|
||||
transition: {
|
||||
enterActiveClass: 'transition duration-100 ease-out',
|
||||
enterFromClass: 'transform scale-95 opacity-0',
|
||||
enterToClass: 'transform scale-100 opacity-100',
|
||||
leaveActiveClass: 'transition duration-75 ease-out',
|
||||
leaveFromClass: 'transform scale-100 opacity-100',
|
||||
leaveToClass: 'transform scale-95 opacity-0'
|
||||
},
|
||||
popperOptions: {
|
||||
placement: 'bottom-end',
|
||||
strategy: 'fixed'
|
||||
}
|
||||
}
|
||||
|
||||
const tabs = {
|
||||
wrapper: 'flex items-center gap-8',
|
||||
base: 'whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm',
|
||||
active: 'border-primary-500 text-primary-600',
|
||||
inactive: 'border-transparent u-text-gray-500 hover:u-text-gray-700 hover:u-border-gray-300'
|
||||
}
|
||||
|
||||
const pills = {
|
||||
wrapper: 'flex items-center gap-4',
|
||||
base: 'px-3 py-2 font-medium text-sm rounded-md',
|
||||
active: 'u-bg-gray-100 u-text-gray-700',
|
||||
inactive: 'u-text-gray-500 hover:u-text-gray-700'
|
||||
}
|
||||
|
||||
const avatar = {
|
||||
wrapper: 'relative inline-flex items-center justify-center',
|
||||
background: 'u-bg-gray-100',
|
||||
rounded: 'rounded-md',
|
||||
placeholder: 'text-xs font-medium leading-none u-text-black truncate',
|
||||
size: {
|
||||
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'
|
||||
},
|
||||
chip: {
|
||||
base: 'absolute block rounded-full ring-2 u-ring-white',
|
||||
position: {
|
||||
'top-right': 'top-0 right-0',
|
||||
'bottom-right': 'bottom-0 right-0',
|
||||
'top-left': 'top-0 left-0',
|
||||
'bottom-left': 'bottom-0 left-0'
|
||||
},
|
||||
variant: {
|
||||
...variantColors.reduce((acc: any, color: string) => {
|
||||
acc[color] = `bg-${color}-400`
|
||||
return acc
|
||||
}, {})
|
||||
},
|
||||
size: {
|
||||
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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const avatarGroup = {
|
||||
ring: 'ring-2 u-ring-white',
|
||||
margin: '-mr-1.5 first:mr-0'
|
||||
}
|
||||
|
||||
const slideover = {
|
||||
wrapper: 'fixed inset-0 flex z-40',
|
||||
overlay: {
|
||||
background: 'bg-gray-500/75 dark:bg-gray-600/75',
|
||||
transition: {
|
||||
enter: 'ease-in-out duration-500',
|
||||
enterFrom: 'opacity-0',
|
||||
enterTo: 'opacity-100',
|
||||
leave: 'ease-in-out duration-500',
|
||||
leaveFrom: 'opacity-100',
|
||||
leaveTo: 'opacity-0'
|
||||
}
|
||||
},
|
||||
base: 'relative flex-1 flex flex-col w-full focus:outline-none',
|
||||
background: 'u-bg-white',
|
||||
width: 'max-w-md',
|
||||
header: 'flex items-center justify-between flex-shrink-0 px-4 sm:px-6 h-16 border-b u-border-gray-200',
|
||||
transition: {
|
||||
enter: 'transform transition ease-in-out duration-500 sm:duration-700',
|
||||
leave: 'transform transition ease-in-out duration-500 sm:duration-700'
|
||||
}
|
||||
}
|
||||
|
||||
const notification = {
|
||||
background: 'u-bg-white',
|
||||
shadow: 'shadow-lg',
|
||||
rounded: 'rounded-lg',
|
||||
ring: 'ring-1 u-ring-gray-200',
|
||||
type: {
|
||||
info: 'i-heroicons-information-circle',
|
||||
success: 'i-heroicons-check-circle',
|
||||
warning: 'i-heroicons-exclamation-circle',
|
||||
error: 'i-heroicons-x-circle'
|
||||
},
|
||||
icon: {
|
||||
base: 'w-6 h-6',
|
||||
color: {
|
||||
warning: 'text-orange-400',
|
||||
info: 'text-blue-400',
|
||||
success: 'text-green-400',
|
||||
error: 'text-red-400'
|
||||
}
|
||||
},
|
||||
close: {
|
||||
icon: {
|
||||
name: 'i-heroicons-x-mark-20-solid'
|
||||
}
|
||||
},
|
||||
transition: {
|
||||
enterActiveClass: 'transform ease-out duration-300 transition',
|
||||
enterFromClass: 'translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2',
|
||||
enterToClass: 'translate-y-0 opacity-100 sm:translate-x-0',
|
||||
leaveActiveClass: 'transition ease-in duration-100',
|
||||
leaveFromClass: 'opacity-100',
|
||||
leaveToClass: 'opacity-0'
|
||||
}
|
||||
}
|
||||
|
||||
const tooltip = {
|
||||
wrapper: 'relative inline-flex',
|
||||
container: 'z-20',
|
||||
width: 'max-w-xs',
|
||||
background: 'u-bg-white',
|
||||
shadow: 'shadow',
|
||||
rounded: 'rounded',
|
||||
ring: 'ring-1 u-ring-gray-200',
|
||||
base: 'invisible lg:visible h-6 px-2 py-1 text-xs font-normal truncate',
|
||||
shortcuts: 'hidden md:inline-flex items-center justify-end flex-shrink-0 gap-0.5 ml-1',
|
||||
transition: {
|
||||
enterActiveClass: 'transition ease-out duration-200',
|
||||
enterFromClass: 'opacity-0 translate-y-1',
|
||||
enterToClass: 'opacity-100 translate-y-0',
|
||||
leaveActiveClass: 'transition ease-in duration-150',
|
||||
leaveFromClass: 'opacity-100 translate-y-0',
|
||||
leaveToClass: 'opacity-0 translate-y-1'
|
||||
},
|
||||
popperOptions: {
|
||||
strategy: 'fixed'
|
||||
}
|
||||
}
|
||||
|
||||
const popover = {
|
||||
wrapper: 'relative',
|
||||
container: 'z-20',
|
||||
width: '',
|
||||
background: 'u-bg-white',
|
||||
shadow: 'shadow-lg',
|
||||
rounded: 'rounded-md',
|
||||
ring: 'ring-1 u-ring-gray-200',
|
||||
base: 'overflow-hidden focus:outline-none',
|
||||
transition: {
|
||||
enterActiveClass: 'transition ease-out duration-200',
|
||||
enterFromClass: 'opacity-0 translate-y-1',
|
||||
enterToClass: 'opacity-100 translate-y-0',
|
||||
leaveActiveClass: 'transition ease-in duration-150',
|
||||
leaveFromClass: 'opacity-100 translate-y-0',
|
||||
leaveToClass: 'opacity-0 translate-y-1'
|
||||
},
|
||||
popperOptions: {
|
||||
strategy: 'fixed'
|
||||
}
|
||||
}
|
||||
|
||||
const contextMenu = {
|
||||
wrapper: 'relative',
|
||||
container: 'z-20',
|
||||
width: '',
|
||||
background: 'u-bg-white',
|
||||
shadow: 'shadow-lg',
|
||||
rounded: 'rounded-md',
|
||||
ring: 'ring-1 u-ring-gray-200',
|
||||
base: 'overflow-hidden focus:outline-none',
|
||||
transition: {
|
||||
enterActiveClass: 'transition ease-out duration-200',
|
||||
enterFromClass: 'opacity-0 translate-y-1',
|
||||
enterToClass: 'opacity-100 translate-y-0',
|
||||
leaveActiveClass: 'transition ease-in duration-150',
|
||||
leaveFromClass: 'opacity-100 translate-y-0',
|
||||
leaveToClass: 'opacity-0 translate-y-1'
|
||||
},
|
||||
popperOptions: {
|
||||
placement: 'bottom-start',
|
||||
scroll: false
|
||||
}
|
||||
}
|
||||
|
||||
const commandPalette = {
|
||||
wrapper: 'flex flex-col flex-1 min-h-0 divide-y divide-gray-100 dark:divide-gray-800',
|
||||
input: {
|
||||
base: 'w-full h-12 pr-4 placeholder-gray-400 dark:placeholder-gray-500 bg-transparent border-0 pl-[3.25rem] u-text-gray-900 focus:ring-0 sm:text-sm',
|
||||
icon: {
|
||||
base: 'pointer-events-none absolute top-3.5 left-5 h-5 w-5 u-text-gray-400',
|
||||
name: 'i-heroicons-magnifying-glass'
|
||||
},
|
||||
close: {
|
||||
base: 'absolute right-2',
|
||||
variant: 'transparent',
|
||||
size: 'md',
|
||||
icon: {
|
||||
name: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
empty: {
|
||||
icon: {
|
||||
name: 'i-heroicons-magnifying-glass'
|
||||
}
|
||||
},
|
||||
option: {
|
||||
selected: {
|
||||
icon: {
|
||||
name: 'i-heroicons-check-20-solid'
|
||||
}
|
||||
},
|
||||
shortcuts: 'hidden md:inline-flex flex-shrink-0 text-xs font-semibold u-text-gray-500'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
card,
|
||||
modal,
|
||||
button,
|
||||
badge,
|
||||
formGroup,
|
||||
input,
|
||||
textarea,
|
||||
select,
|
||||
selectCustom,
|
||||
checkbox,
|
||||
radio,
|
||||
container,
|
||||
toggle,
|
||||
verticalNavigation,
|
||||
alertDialog,
|
||||
dropdown,
|
||||
tabs,
|
||||
pills,
|
||||
avatar,
|
||||
avatarGroup,
|
||||
slideover,
|
||||
notification,
|
||||
tooltip,
|
||||
popover,
|
||||
contextMenu,
|
||||
commandPalette
|
||||
}
|
||||
}
|
||||
|
||||
export type DefaultPreset = ReturnType<typeof defaultPreset>
|
||||
@@ -1,71 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.dark {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
a:focus {
|
||||
@apply outline-primary-500;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.u-bg-white { @apply bg-white dark:bg-black; }
|
||||
.u-bg-gray-50 { @apply bg-gray-50 dark:bg-gray-900; }
|
||||
.u-bg-gray-100 { @apply bg-gray-100 dark:bg-gray-800; }
|
||||
.u-bg-gray-200 { @apply bg-gray-200 dark:bg-gray-700; }
|
||||
.u-bg-gray-300 { @apply bg-gray-300 dark:bg-gray-600; }
|
||||
.u-bg-gray-400 { @apply bg-gray-400 dark:bg-gray-500; }
|
||||
.u-bg-gray-500 { @apply bg-gray-500 dark:bg-gray-400; }
|
||||
.u-bg-gray-600 { @apply bg-gray-600 dark:bg-gray-300; }
|
||||
.u-bg-gray-700 { @apply bg-gray-700 dark:bg-gray-200; }
|
||||
.u-bg-gray-800 { @apply bg-gray-800 dark:bg-gray-100; }
|
||||
.u-bg-gray-900 { @apply bg-gray-900 dark:bg-gray-50; }
|
||||
.u-bg-black { @apply bg-black dark:bg-white; }
|
||||
.u-text-white { @apply text-white dark:text-black; }
|
||||
.u-text-gray-50 { @apply text-gray-50 dark:text-gray-900; }
|
||||
.u-text-gray-100 { @apply text-gray-100 dark:text-gray-800; }
|
||||
.u-text-gray-200 { @apply text-gray-200 dark:text-gray-700; }
|
||||
.u-text-gray-300 { @apply text-gray-300 dark:text-gray-600; }
|
||||
.u-text-gray-400 { @apply text-gray-400 dark:text-gray-500; }
|
||||
.u-text-gray-500 { @apply text-gray-500 dark:text-gray-400; }
|
||||
.u-text-gray-600 { @apply text-gray-600 dark:text-gray-300; }
|
||||
.u-text-gray-700 { @apply text-gray-700 dark:text-gray-200; }
|
||||
.u-text-gray-800 { @apply text-gray-800 dark:text-gray-100; }
|
||||
.u-text-gray-900 { @apply text-gray-900 dark:text-gray-50; }
|
||||
.u-text-black { @apply text-black dark:text-white; }
|
||||
.u-border-white { @apply border-white dark:border-black; }
|
||||
.u-border-gray-100 { @apply border-gray-100 dark:border-gray-900; }
|
||||
.u-border-gray-200 { @apply border-gray-200 dark:border-gray-800; }
|
||||
.u-border-gray-300 { @apply border-gray-300 dark:border-gray-700; }
|
||||
.u-border-gray-400 { @apply border-gray-400 dark:border-gray-600; }
|
||||
.u-border-gray-500 { @apply border-gray-500 dark:border-gray-500; }
|
||||
.u-border-gray-600 { @apply border-gray-600 dark:border-gray-400; }
|
||||
.u-border-gray-700 { @apply border-gray-700 dark:border-gray-300; }
|
||||
.u-border-gray-800 { @apply border-gray-800 dark:border-gray-200; }
|
||||
.u-border-gray-900 { @apply border-gray-900 dark:border-gray-100; }
|
||||
.u-border-black { @apply border-black dark:border-white; }
|
||||
.u-divide-white { @apply divide-white dark:divide-black; }
|
||||
.u-divide-gray-100 { @apply divide-gray-100 dark:divide-gray-900; }
|
||||
.u-divide-gray-200 { @apply divide-gray-200 dark:divide-gray-800; }
|
||||
.u-divide-gray-300 { @apply divide-gray-300 dark:divide-gray-700; }
|
||||
.u-divide-gray-400 { @apply divide-gray-400 dark:divide-gray-600; }
|
||||
.u-divide-gray-500 { @apply divide-gray-500 dark:divide-gray-500; }
|
||||
.u-divide-gray-600 { @apply divide-gray-600 dark:divide-gray-400; }
|
||||
.u-divide-gray-700 { @apply divide-gray-700 dark:divide-gray-300; }
|
||||
.u-divide-gray-800 { @apply divide-gray-800 dark:divide-gray-200; }
|
||||
.u-divide-gray-900 { @apply divide-gray-900 dark:divide-gray-100; }
|
||||
.u-divide-black { @apply divide-black dark:divide-white; }
|
||||
.u-ring-white { @apply ring-white dark:ring-black; }
|
||||
.u-ring-gray-100 { @apply ring-gray-100 dark:ring-gray-900; }
|
||||
.u-ring-gray-200 { @apply ring-gray-200 dark:ring-gray-800; }
|
||||
.u-ring-gray-300 { @apply ring-gray-300 dark:ring-gray-700; }
|
||||
.u-ring-gray-400 { @apply ring-gray-400 dark:ring-gray-600; }
|
||||
.u-ring-gray-500 { @apply ring-gray-500 dark:ring-gray-500; }
|
||||
.u-ring-gray-600 { @apply ring-gray-600 dark:ring-gray-400; }
|
||||
.u-ring-gray-700 { @apply ring-gray-700 dark:ring-gray-300; }
|
||||
.u-ring-gray-800 { @apply ring-gray-800 dark:ring-gray-200; }
|
||||
.u-ring-gray-900 { @apply ring-gray-900 dark:ring-gray-100; }
|
||||
.u-ring-black { @apply ring-black dark:ring-white; }
|
||||
}
|
||||
6
src/runtime/types/avatar.d.ts
vendored
6
src/runtime/types/avatar.d.ts
vendored
@@ -1,9 +1,9 @@
|
||||
export interface Avatar {
|
||||
src: string
|
||||
src: string | boolean
|
||||
alt: string
|
||||
text: string
|
||||
size: string
|
||||
rounded: boolean
|
||||
chip: string
|
||||
chipColor: string
|
||||
chipVariant: string
|
||||
chipPosition: string
|
||||
}
|
||||
|
||||
18
src/runtime/types/button.d.ts
vendored
Normal file
18
src/runtime/types/button.d.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
export interface Button {
|
||||
type: string
|
||||
block: boolean
|
||||
label: string
|
||||
loading: boolean
|
||||
disabled: boolean
|
||||
size: string
|
||||
color: string
|
||||
variant: string
|
||||
icon: string
|
||||
leadingIcon: string
|
||||
trailingIcon: string
|
||||
trailing: boolean
|
||||
to: string | object
|
||||
target: string
|
||||
square: boolean
|
||||
truncate: boolean
|
||||
}
|
||||
1
src/runtime/types/command-palette.d.ts
vendored
1
src/runtime/types/command-palette.d.ts
vendored
@@ -1,4 +1,3 @@
|
||||
import type { UseFuseOptions } from '@vueuse/integrations/useFuse'
|
||||
import type { FuseSortFunctionMatch, FuseSortFunctionMatchList } from 'fuse.js'
|
||||
import type { Avatar } from './avatar'
|
||||
|
||||
|
||||
2
src/runtime/types/index.d.ts
vendored
2
src/runtime/types/index.d.ts
vendored
@@ -3,3 +3,5 @@ export * from './clipboard'
|
||||
export * from './command-palette'
|
||||
export * from './popper'
|
||||
export * from './toast'
|
||||
|
||||
export type DeepPartial<T> = Partial<{ [P in keyof T]: DeepPartial<T[P]> | { [key: string]: string } }>
|
||||
|
||||
5
src/runtime/types/toast.d.ts
vendored
5
src/runtime/types/toast.d.ts
vendored
@@ -1,5 +1,6 @@
|
||||
export interface ToastNotificationAction {
|
||||
label: string,
|
||||
import type { Button } from './button'
|
||||
|
||||
export interface ToastNotificationAction extends Partial<Button> {
|
||||
click: Function
|
||||
}
|
||||
|
||||
|
||||
11
src/runtime/ui.css
Normal file
11
src/runtime/ui.css
Normal file
@@ -0,0 +1,11 @@
|
||||
.dark {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
a:focus-visible {
|
||||
@apply outline-primary-500 dark:outline-primary-400;
|
||||
}
|
||||
|
||||
select {
|
||||
background-image: none;
|
||||
}
|
||||
@@ -10,7 +10,8 @@ export const colorsToExclude = [
|
||||
'gray',
|
||||
'zinc',
|
||||
'neutral',
|
||||
'stone'
|
||||
'stone',
|
||||
'cool'
|
||||
]
|
||||
|
||||
export const excludeColors = (colors: object) => Object.keys(omit(colors, colorsToExclude)).map(color => kebabCase(color)) as string[]
|
||||
|
||||
Reference in New Issue
Block a user