feat!: handle color states on form elements (#234)

This commit is contained in:
Benjamin Canac
2023-05-26 22:07:49 +02:00
committed by GitHub
parent 1a9dc5c980
commit 9ce531a06f
21 changed files with 598 additions and 274 deletions

View File

@@ -5,7 +5,7 @@
v-model="primary"
name="primary"
class="w-full [&>div>button]:!rounded-r-none"
appearance="gray"
color="gray"
:ui="{ width: 'w-[194px]' }"
:popper="{ placement: 'bottom-start' }"
:options="primaryOptions"
@@ -29,7 +29,7 @@
v-model="gray"
name="gray"
class="w-full [&>div>button]:!rounded-l-none [&>div>button]:-ml-px"
appearance="gray"
color="gray"
:ui="{ width: 'w-[194px]' }"
:popper="{ placement: 'bottom-end' }"
:options="grayOptions"

View File

@@ -7,7 +7,7 @@
v-if="prop.type === 'boolean'"
v-model="componentProps[prop.name]"
:name="prop.name"
appearance="none"
variant="none"
class="justify-center"
/>
<USelectMenu
@@ -16,7 +16,7 @@
:options="prop.options"
:name="prop.name"
:label="componentProps[prop.name]"
appearance="none"
variant="none"
class="inline-flex"
:ui="{ width: 'w-32 !-mt-px', rounded: 'rounded-b-md' }"
:ui-select="{ custom: '!py-0' }"
@@ -27,7 +27,7 @@
:model-value="componentProps[prop.name]"
:type="prop.type === 'number' ? 'number' : 'text'"
:name="prop.name"
appearance="none"
variant="none"
autocomplete="off"
:ui="{ custom: '!py-0' }"
@update:model-value="val => componentProps[prop.name] = prop.type === 'number' ? Number(val) : val"
@@ -79,6 +79,10 @@ const props = defineProps({
type: Array,
default: () => []
},
extraColors: {
type: Array,
default: () => []
},
backgroundClass: {
type: String,
default: 'bg-white dark:bg-gray-900'
@@ -121,7 +125,8 @@ const propsToSelect = computed(() => Object.keys(componentProps).map((key) => {
const keys = useGet(ui.value, dottedKey, {})
let options = typeof keys === 'object' && Object.keys(keys)
if (key.toLowerCase().endsWith('color')) {
options = appConfig.ui.colors
// @ts-ignore
options = [...appConfig.ui.colors, ...props.extraColors]
}
return {

View File

@@ -36,6 +36,8 @@ Use the `chip-color` and `chip-position` props to display a chip on the Avatar.
props:
chipColor: 'primary'
chipPosition: 'top-right'
extraColors:
- gray
baseProps:
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
alt: 'Avatar'

View File

@@ -12,6 +12,53 @@ baseProps:
---
::
### Style
Use the `color` and `variant` props to change the visual style of the Input.
::component-card
---
baseProps:
name: 'input'
placeholder: 'Search...'
props:
color: 'primary'
variant: 'outline'
---
::
Besides all the colors from the `ui.colors` object, you can also use the `white` (default) and `gray` colors with their pre-defined variants.
#### White
::component-card
---
baseProps:
name: 'input'
placeholder: 'Search...'
props:
color: 'white'
variant: 'outline'
excludedProps:
- color
---
::
#### Gray
::component-card
---
baseProps:
name: 'input'
placeholder: 'Search...'
props:
color: 'gray'
variant: 'outline'
excludedProps:
- color
---
::
### Size
Use the `size` prop to change the size of the Input.
@@ -38,20 +85,6 @@ props:
---
::
### Appearance
Use the `appearance` prop to change the style of the Input.
::component-card
---
baseProps:
name: 'input'
placeholder: 'Search...'
props:
appearance: 'white'
---
::
### Icon
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
@@ -65,9 +98,12 @@ baseProps:
placeholder: 'Search...'
props:
icon: 'i-heroicons-magnifying-glass-20-solid'
appearance: 'white'
size: 'sm'
color: 'white'
trailing: false
extraColors:
- white
- gray
excludedProps:
- icon
---
@@ -81,12 +117,9 @@ Use the `disabled` prop to disable the Input.
---
baseProps:
name: 'input'
props:
placeholder: 'Search...'
appearance: 'white'
props:
disabled: true
excludedProps:
- placeholder
---
::
@@ -109,32 +142,6 @@ excludedProps:
---
::
### Group
You can use the `InputGroup` component to add a label and additional informations to a form element.
::component-card{slug="InputGroup"}
---
baseProps:
name: 'group'
props:
label: 'Email'
help: "We'll only use this for spam."
hint: 'Required'
required: true
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
---
#default
:u-input{name="group" placeholder="you@example.com" icon="i-heroicons-envelope"}
::
::alert{icon="i-heroicons-light-bulb"}
This also works with `Textarea`, `Select` and `SelectMenu` components.
::
## Props
:component-props

View File

@@ -12,6 +12,53 @@ baseProps:
---
::
### Style
Use the `color` and `variant` props to change the visual style of the Textarea.
::component-card
---
baseProps:
name: 'textarea'
placeholder: 'Search...'
props:
color: 'primary'
variant: 'outline'
---
::
Besides all the colors from the `ui.colors` object, you can also use the `white` (default) and `gray` colors with their pre-defined variants.
#### White
::component-card
---
baseProps:
name: 'textarea'
placeholder: 'Search...'
props:
color: 'white'
variant: 'outline'
excludedProps:
- color
---
::
#### Gray
::component-card
---
baseProps:
name: 'textarea'
placeholder: 'Search...'
props:
color: 'gray'
variant: 'outline'
excludedProps:
- color
---
::
### Size
Use the `size` prop to change the size of the Textarea.
@@ -38,20 +85,6 @@ props:
---
::
### Appearance
Use the `appearance` prop to change the style of the Textarea.
::component-card
---
baseProps:
name: 'textarea'
placeholder: 'Search...'
props:
appearance: 'white'
---
::
### Disabled
Use the `disabled` prop to disable the Textarea.
@@ -62,7 +95,6 @@ baseProps:
name: 'input'
placeholder: 'Search...'
props:
appearance: 'white'
disabled: true
---
::

View File

@@ -22,9 +22,65 @@ excludedProps:
---
::
### Style
Use the `color` and `variant` props to change the visual style of the Select.
::component-card
---
baseProps:
name: 'select'
options:
- 'United States'
- 'Canada'
- 'Mexico'
props:
color: 'primary'
variant: 'outline'
---
::
Besides all the colors from the `ui.colors` object, you can also use the `white` (default) and `gray` colors with their pre-defined variants.
#### White
::component-card
---
baseProps:
name: 'select'
options:
- 'United States'
- 'Canada'
- 'Mexico'
props:
color: 'white'
variant: 'outline'
excludedProps:
- color
---
::
#### Gray
::component-card
---
baseProps:
name: 'select'
options:
- 'United States'
- 'Canada'
- 'Mexico'
props:
color: 'gray'
variant: 'outline'
excludedProps:
- color
---
::
### Size
Use the `size` prop to change the size of the Input.
Use the `size` prop to change the size of the Select.
::component-card
---
@@ -56,24 +112,6 @@ props:
---
::
### Appearance
Use the `appearance` prop to change the style of the Select.
::component-card
---
baseProps:
name: 'select'
options:
- 'United States'
- 'Canada'
- 'Mexico'
placeholder: 'Search...'
props:
appearance: 'white'
---
::
### Icon
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
@@ -91,8 +129,11 @@ baseProps:
placeholder: 'Search...'
props:
icon: 'i-heroicons-magnifying-glass-20-solid'
appearance: 'white'
color: 'white'
size: 'sm'
extraColors:
- white
- gray
excludedProps:
- icon
---
@@ -100,7 +141,7 @@ excludedProps:
### Disabled
Use the `disabled` prop to disable the Input.
Use the `disabled` prop to disable the Select.
::component-card
---
@@ -112,7 +153,6 @@ baseProps:
- 'Mexico'
placeholder: 'Search...'
props:
appearance: 'white'
disabled: true
---
::

View File

@@ -8,7 +8,7 @@ headlessui:
## Usage
The SelectMenu component renders by default a [Select](/forms/select) component and is based on the `ui.select` preset. You can use most of the Select props to configure the display if you don't want to override the default slot such as [size](/forms/select#size), [placeholder](/forms/select#placeholder), [appearance](/forms/select#appearance), [icon](/forms/select#icon), [disabled](/forms/select#disabled), etc.
The SelectMenu component renders by default a [Select](/forms/select) component and is based on the `ui.select` preset. You can use most of the Select props to configure the display if you don't want to override the default slot such as [color](/forms/select#style), [variant](/forms/select#style), [size](/forms/select#size), [placeholder](/forms/select#placeholder), [icon](/forms/select#icon), [disabled](/forms/select#disabled), etc.
Like the Select component, you can use the `options` prop to pass an array of strings or objects.
@@ -148,6 +148,10 @@ baseProps:
options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
props:
icon: 'i-heroicons-magnifying-glass-20-solid'
color: 'white'
extraColors:
- white
- gray
excludedProps:
- icon
---
@@ -157,6 +161,8 @@ excludedProps:
Use the `searchable` prop to enable search.
Use the `searchable-placeholder` prop to set a different placeholder.
This will use Headless UI [Combobox](https://headlessui.com/vue/combobox) component instead of [Listbox](https://headlessui.com/vue/listbox).
::component-card
@@ -167,6 +173,7 @@ baseProps:
options: ['Wade Cooper', 'Arlene Mccoy', 'Devon Webb', 'Tom Cook', 'Tanya Fox', 'Hellen Schmidt', 'Caroline Schultz', 'Mason Heaney', 'Claudie Smitham', 'Emil Schaefer']
props:
searchable: true
searchablePlaceholder: 'Search a person...'
---
::

View File

@@ -0,0 +1,140 @@
---
github: true
description: Display a label and additional informations around a form element.
---
## Usage
Use the FormGroup component around an [Input](/forms/input), [Textarea](/forms/textarea), [Select](/forms/select) or a [SelectMenu](/forms/select-menu) with the `name` prop to automatically associate a `<label>` element with the form element.
::component-card
---
props:
name: 'email'
label: 'Email'
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
---
#default
:u-input{placeholder="you@example.com" icon="i-heroicons-envelope"}
::
### Required
Use the `required` prop to indicate that the form element is required.
::component-card
---
baseProps:
name: 'group-required'
props:
label: 'Email'
required: true
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
---
#default
:u-input{placeholder="you@example.com" icon="i-heroicons-envelope"}
::
### Description
Use the `description` prop to display a description below the label.
::component-card
---
baseProps:
name: 'group-description'
props:
label: 'Email'
description: "We'll only use this for spam."
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
---
#default
:u-input{placeholder="you@example.com" icon="i-heroicons-envelope"}
::
### Hint
Use the `hint` prop to display a hint above the form element.
::component-card
---
baseProps:
name: 'group-hint'
props:
label: 'Email'
hint: 'Optional'
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
---
#default
:u-input{placeholder="you@example.com" icon="i-heroicons-envelope"}
::
### Help
Use the `help` prop to display an help message below the form element.
::component-card
---
baseProps:
name: 'group-help'
props:
label: 'Email'
help: 'We will never share your email with anyone else.'
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
---
#default
:u-input{placeholder="you@example.com" icon="i-heroicons-envelope"}
::
### Error
Use the `error` prop to display an error message below the form element.
When used together with the `help` prop, the `error` prop will take precedence.
::component-card
---
baseProps:
name: 'group-error'
props:
label: 'Email'
help: 'We will never share your email with anyone else.'
error: "Not a valid email address."
code: >-
<UInput placeholder="you@example.com" trailing-icon="i-heroicons-exclamation-triangle-20-solid" />
---
#default
:u-input{model-value="benjamincanac" placeholder="you@example.com" trailing-icon="i-heroicons-exclamation-triangle-20-solid"}
::
You can also use the `error` prop as a boolean to mark the form element as invalid.
::alert{icon="i-heroicons-light-bulb"}
The `error` prop will automatically set the `color` prop of the form element to `red`.
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -132,6 +132,8 @@ baseProps:
props:
icon: 'i-heroicons-x-circle'
color: 'red'
extraColors:
- gray
excludedProps:
- icon
---

View File

@@ -121,45 +121,49 @@ export default defineNuxtModule<ModuleOptions>({
}
tailwindConfig.safelist = tailwindConfig.safelist || []
tailwindConfig.safelist.push(...['bg-gray-400', {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(50|400|500)`)
}, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-500`),
variants: ['disabled']
}, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(400|950)`),
variants: ['dark']
}, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(500|900|950)`),
variants: ['dark:hover']
}, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-400`),
variants: ['dark:disabled']
}, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(50|100|600)`),
variants: ['hover']
}, {
pattern: new RegExp(`outline-(${safeColorsAsRegex})-500`),
variants: ['focus-visible']
}, {
pattern: new RegExp(`outline-(${safeColorsAsRegex})-400`),
variants: ['dark:focus-visible']
}, {
pattern: new RegExp(`ring-(${safeColorsAsRegex})-500`),
variants: ['focus-visible']
}, {
pattern: new RegExp(`ring-(${safeColorsAsRegex})-400`),
variants: ['dark', 'dark:focus-visible']
}, {
pattern: new RegExp(`text-(${safeColorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`text-(${safeColorsAsRegex})-600`),
variants: ['hover']
}, {
pattern: new RegExp(`text-(${safeColorsAsRegex})-500`),
variants: ['dark:hover']
}])
tailwindConfig.safelist.push(...[
'bg-gray-500',
'dark:bg-gray-400',
{
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(50|400|500)`)
}, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-500`),
variants: ['disabled']
}, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(400|950)`),
variants: ['dark']
}, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(500|900|950)`),
variants: ['dark:hover']
}, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-400`),
variants: ['dark:disabled']
}, {
pattern: new RegExp(`bg-(${safeColorsAsRegex})-(50|100|600)`),
variants: ['hover']
}, {
pattern: new RegExp(`outline-(${safeColorsAsRegex})-500`),
variants: ['focus-visible']
}, {
pattern: new RegExp(`outline-(${safeColorsAsRegex})-400`),
variants: ['dark:focus-visible']
}, {
pattern: new RegExp(`ring-(${safeColorsAsRegex})-500`),
variants: ['focus', 'focus-visible']
}, {
pattern: new RegExp(`ring-(${safeColorsAsRegex})-400`),
variants: ['dark', 'dark:focus', 'dark:focus-visible']
}, {
pattern: new RegExp(`text-(${safeColorsAsRegex})-400`),
variants: ['dark']
}, {
pattern: new RegExp(`text-(${safeColorsAsRegex})-500`),
variants: ['dark:hover']
}, {
pattern: new RegExp(`text-(${safeColorsAsRegex})-600`),
variants: ['hover']
}
])
tailwindConfig.plugins = tailwindConfig.plugins || []
tailwindConfig.plugins.push(iconsPlugin({ collections: getIconCollections(options.icons as any[]) }))

View File

@@ -218,7 +218,9 @@ const kbd = {
const input = {
wrapper: 'relative',
base: 'relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none',
base: 'relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0',
rounded: 'rounded-md',
placeholder: 'placeholder-gray-400 dark:placeholder-gray-500',
custom: '',
size: {
'2xs': 'text-xs',
@@ -264,13 +266,21 @@ const input = {
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 placeholder:text-gray-400 dark:placeholder:text-gray-500'
color: {
white: {
outline: 'shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400',
},
gray: {
outline: 'shadow-sm bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400',
}
},
variant: {
outline: 'shadow-sm bg-transparent text-gray-900 dark:text-white ring-1 ring-inset ring-{color}-500 dark:ring-{color}-400 focus:ring-2 focus:ring-{color}-500 dark:focus:ring-{color}-400',
none: 'bg-transparent focus:ring-0 focus:shadow-none'
},
icon: {
base: 'text-gray-400 dark:text-gray-500',
base: 'flex-shrink-0 text-gray-400 dark:text-gray-500',
color: 'text-{color}-500 dark:text-{color}-400',
size: {
'2xs': 'h-3.5 w-3.5',
xs: 'h-4 w-4',
@@ -304,27 +314,32 @@ const input = {
},
default: {
size: 'sm',
appearance: 'white',
color: 'white',
variant: 'outline',
loadingIcon: 'i-heroicons-arrow-path-20-solid'
}
}
const inputGroup = {
const formGroup = {
wrapper: '',
label: 'block text-sm font-medium text-gray-700 dark:text-gray-200',
labelWrapper: 'flex content-center justify-between',
label: {
wrapper: 'flex content-center justify-between',
base: 'block text-sm font-medium text-gray-700 dark:text-gray-200',
required: `after:content-['*'] after:ml-0.5 after:text-red-500 dark:after:text-red-400`
},
description: 'text-sm text-gray-500 dark:text-gray-400',
container: 'mt-1 relative',
required: 'text-red-500 dark:text-red-400 ml-0.5',
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'
hint: 'text-sm text-gray-500 dark:text-gray-400',
help: 'mt-2 text-sm text-gray-500 dark:text-gray-400',
error: 'mt-2 text-sm text-red-500 dark:text-red-400'
}
const textarea = {
...input,
default: {
size: 'sm',
appearance: 'white'
color: 'white',
variant: 'outline',
}
}
@@ -332,7 +347,8 @@ const select = {
...input,
default: {
size: 'sm',
appearance: 'white',
color: 'white',
variant: 'outline',
trailingIcon: 'i-heroicons-chevron-down-20-solid'
}
}
@@ -701,7 +717,7 @@ const notification = {
padding: 'p-4',
ring: 'ring-1 ring-gray-200 dark:ring-gray-800',
icon: {
base: 'flex-shrink-0 w-5 h-5',
base: 'flex-shrink-0 w-5 h-5 text-gray-400 dark:text-gray-500',
color: 'text-{color}-500 dark:text-{color}-400'
},
avatar: {
@@ -753,7 +769,7 @@ export default {
dropdown,
kbd,
input,
inputGroup,
formGroup,
textarea,
select,
selectMenu,

View File

@@ -1,7 +1,7 @@
import { h, computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { classNames } from '../../utils'
import { classNames, getSlotsChildren } from '../../utils'
import Avatar from './Avatar.vue'
import { useAppConfig } from '#imports'
// TODO: Remove
@@ -34,20 +34,7 @@ export default defineComponent({
const ui = computed<Partial<typeof appConfig.ui.avatarGroup>>(() => defu({}, props.ui, appConfig.ui.avatarGroup))
const children = computed(() => {
let children = slots.default?.()
if (children.length) {
if (typeof children[0].type === 'symbol') {
// @ts-ignore-next
children = children[0].children
// @ts-ignore-next
} else if (children[0].type.name === 'ContentSlot') {
// @ts-ignore-next
children = children[0].ctx.slots.default?.()
}
}
return children
})
const children = computed(() => getSlotsChildren(slots))
const max = computed(() => typeof props.max === 'string' ? parseInt(props.max, 10) : props.max)

View File

@@ -1,6 +1,7 @@
import { h, computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { getSlotsChildren } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
@@ -28,20 +29,7 @@ export default defineComponent({
const ui = computed<Partial<typeof appConfig.ui.buttonGroup>>(() => defu({}, props.ui, appConfig.ui.buttonGroup))
const children = computed(() => {
let children = slots.default?.()
if (children.length) {
if (typeof children[0].type === 'symbol') {
// @ts-ignore-next
children = children[0].children
// @ts-ignore-next
} else if (children[0].type.name === 'ContentSlot') {
// @ts-ignore-next
children = children[0].ctx.slots.default?.()
}
}
return children
})
const children = computed(() => getSlotsChildren(slots))
const rounded = computed(() => ({
'rounded-none': { left: 'rounded-l-none', right: 'rounded-r-none' },

View File

@@ -0,0 +1,82 @@
import { computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { getSlotsChildren } from '../../utils'
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
},
error: {
type: [String, Boolean],
default: null
},
hint: {
type: String,
default: null
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.formGroup>>,
default: () => appConfig.ui.formGroup
}
},
setup (props, { slots }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.formGroup>>(() => defu({}, props.ui, appConfig.ui.formGroup))
const children = computed(() => getSlotsChildren(slots))
const clones = computed(() => children.value.map((node) => {
if (props.error) {
node.props.oldColor = node.props.color
node.props.color = 'red'
} else {
node.props.color = node.props.oldColor
}
if (props.name) {
node.props.name = props.name
}
return node
}))
return () => h('div', { class: [ui.value.wrapper] }, [
props.label && h('div', { class: [ui.value.label.wrapper] }, [
h('label', { for: props.name, class: [ui.value.label.base, props.required && ui.value.label.required] }, props.label),
props.hint && h('span', { class: [ui.value.hint] }, props.hint)
]),
props.description && h('p', { class: [ui.value.description] }, props.description),
h('div', { class: [!!props.label && ui.value.container] }, [
...clones.value,
props.error && typeof props.error === 'string' ? h('p', { class: [ui.value.error] }, props.error) : props.help ? h('p', { class: [ui.value.help] }, props.help) : null
])
])
}
})

View File

@@ -113,6 +113,10 @@ export default defineComponent({
type: Boolean,
default: false
},
padded: {
type: Boolean,
default: true
},
size: {
type: String,
default: () => appConfig.ui.input.default.size,
@@ -120,11 +124,21 @@ export default defineComponent({
return Object.keys(appConfig.ui.input.size).includes(value)
}
},
appearance: {
color: {
type: String,
default: () => appConfig.ui.input.default.appearance,
default: () => appConfig.ui.input.default.color,
validator (value: string) {
return Object.keys(appConfig.ui.input.appearance).includes(value)
return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.input.color)].includes(value)
}
},
variant: {
type: String,
default: () => appConfig.ui.input.default.variant,
validator (value: string) {
return [
...Object.keys(appConfig.ui.input.variant),
...Object.values(appConfig.ui.input.color).flatMap(value => Object.keys(value))
].includes(value)
}
},
ui: {
@@ -158,11 +172,15 @@ export default defineComponent({
})
const inputClass = 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.rounded,
ui.value.placeholder,
ui.value.size[props.size],
ui.value.padding[props.size],
ui.value.appearance[props.appearance],
props.padded && ui.value.padding[props.size],
variant?.replaceAll('{color}', props.color),
isLeading.value && ui.value.leading.padding[props.size],
isTrailing.value && ui.value.trailing.padding[props.size],
ui.value.custom
@@ -196,6 +214,7 @@ export default defineComponent({
const iconClass = computed(() => {
return classNames(
ui.value.icon.base,
appConfig.ui.colors.includes(props.color) && ui.value.icon.color.replaceAll('{color}', props.color),
ui.value.icon.size[props.size],
props.loading && 'animate-spin'
)

View File

@@ -1,78 +0,0 @@
<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>

View File

@@ -97,6 +97,10 @@ export default defineComponent({
type: Array,
default: () => []
},
padded: {
type: Boolean,
default: true
},
size: {
type: String,
default: () => appConfig.ui.select.default.size,
@@ -104,11 +108,21 @@ export default defineComponent({
return Object.keys(appConfig.ui.select.size).includes(value)
}
},
appearance: {
color: {
type: String,
default: () => appConfig.ui.select.default.appearance,
default: () => appConfig.ui.select.default.color,
validator (value: string) {
return Object.keys(appConfig.ui.select.appearance).includes(value)
return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.select.color)].includes(value)
}
},
variant: {
type: String,
default: () => appConfig.ui.select.default.variant,
validator (value: string) {
return [
...Object.keys(appConfig.ui.select.variant),
...Object.values(appConfig.ui.select.color).flatMap(value => Object.keys(value))
].includes(value)
}
},
textAttribute: {
@@ -188,11 +202,15 @@ export default defineComponent({
})
const selectClass = 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.rounded,
ui.value.placeholder,
ui.value.size[props.size],
ui.value.padding[props.size],
ui.value.appearance[props.appearance],
props.padded && ui.value.padding[props.size],
variant?.replaceAll('{color}', props.color),
!!props.icon && ui.value.leading.padding[props.size],
ui.value.trailing.padding[props.size],
ui.value.custom
@@ -202,6 +220,7 @@ export default defineComponent({
const iconClass = computed(() => {
return classNames(
ui.value.icon.base,
appConfig.ui.colors.includes(props.color) && ui.value.icon.color.replaceAll('{color}', props.color),
ui.value.icon.size[props.size]
)
})

View File

@@ -198,6 +198,10 @@ export default defineComponent({
type: String,
default: null
},
padded: {
type: Boolean,
default: true
},
size: {
type: String,
default: () => appConfig.ui.select.default.size,
@@ -205,11 +209,21 @@ export default defineComponent({
return Object.keys(appConfig.ui.select.size).includes(value)
}
},
appearance: {
color: {
type: String,
default: () => appConfig.ui.select.default.appearance,
default: () => appConfig.ui.select.default.color,
validator (value: string) {
return Object.keys(appConfig.ui.select.appearance).includes(value)
return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.select.color)].includes(value)
}
},
variant: {
type: String,
default: () => appConfig.ui.select.default.variant,
validator (value: string) {
return [
...Object.keys(appConfig.ui.select.variant),
...Object.values(appConfig.ui.select.color).flatMap(value => Object.keys(value))
].includes(value)
}
},
optionAttribute: {
@@ -249,13 +263,17 @@ export default defineComponent({
const searchInput = ref<ComponentPublicInstance<HTMLElement>>()
const selectMenuClass = computed(() => {
const variant = uiSelect.value.color?.[props.color as string]?.[props.variant as string] || uiSelect.value.variant[props.variant]
return classNames(
uiSelect.value.base,
uiSelect.value.rounded,
uiSelect.value.placeholder,
'text-left cursor-default',
uiSelect.value.size[props.size],
uiSelect.value.gap[props.size],
uiSelect.value.padding[props.size],
uiSelect.value.appearance[props.appearance],
props.padded && uiSelect.value.padding[props.size],
variant?.replaceAll('{color}', props.color),
!!props.icon && uiSelect.value.leading.padding[props.size],
uiSelect.value.trailing.padding[props.size],
uiSelect.value.custom,
@@ -266,6 +284,7 @@ export default defineComponent({
const iconClass = computed(() => {
return classNames(
uiSelect.value.icon.base,
appConfig.ui.colors.includes(props.color) && uiSelect.value.icon.color.replaceAll('{color}', props.color),
uiSelect.value.icon.size[props.size]
)
})

View File

@@ -72,6 +72,10 @@ export default defineComponent({
type: Boolean,
default: false
},
padded: {
type: Boolean,
default: true
},
size: {
type: String,
default: () => appConfig.ui.textarea.default.size,
@@ -79,11 +83,21 @@ export default defineComponent({
return Object.keys(appConfig.ui.textarea.size).includes(value)
}
},
appearance: {
color: {
type: String,
default: () => appConfig.ui.textarea.default.appearance,
default: () => appConfig.ui.textarea.default.color,
validator (value: string) {
return Object.keys(appConfig.ui.textarea.appearance).includes(value)
return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.textarea.color)].includes(value)
}
},
variant: {
type: String,
default: () => appConfig.ui.textarea.default.variant,
validator (value: string) {
return [
...Object.keys(appConfig.ui.textarea.variant),
...Object.values(appConfig.ui.textarea.color).flatMap(value => Object.keys(value))
].includes(value)
}
},
ui: {
@@ -146,11 +160,15 @@ export default defineComponent({
})
const textareaClass = 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.rounded,
ui.value.placeholder,
ui.value.size[props.size],
ui.value.padding[props.size],
ui.value.appearance[props.appearance],
props.padded && ui.value.padding[props.size],
variant?.replaceAll('{color}', props.color),
!props.resize && 'resize-none',
ui.value.custom
)

View File

@@ -134,7 +134,7 @@ export default defineComponent({
const iconClass = computed(() => {
return classNames(
ui.value.icon.base,
ui.value.icon.color?.replaceAll('{color}', props.color)
appConfig.ui.colors.includes(props.color) && ui.value.icon.color?.replaceAll('{color}', props.color)
)
})

View File

@@ -14,3 +14,18 @@ export const omit = (obj: object, keys: string[]) => {
Object.entries(obj).filter(([key]) => !keys.includes(key))
)
}
export const getSlotsChildren = (slots: any) => {
let children = slots.default?.()
if (children.length) {
if (typeof children[0].type === 'symbol') {
// @ts-ignore-next
children = children[0].children
// @ts-ignore-next
} else if (children[0].type.name === 'ContentSlot') {
// @ts-ignore-next
children = children[0].ctx.slots.default?.()
}
}
return children
}