mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-20 23:11:43 +01:00
feat(Checkbox): new component (#67)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import type { AccordionRootProps, AccordionRootEmits } from 'radix-vue'
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/accordion'
|
||||
import type { IconProps } from '#ui/components/Icon.vue'
|
||||
|
||||
const appConfig = _appConfig as AppConfig & { ui: { accordion: Partial<typeof theme> } }
|
||||
|
||||
@@ -11,7 +12,7 @@ const accordion = tv({ extend: tv(theme), ...(appConfig.ui?.accordion || {}) })
|
||||
|
||||
export interface AccordionItem {
|
||||
slot?: string
|
||||
icon?: string
|
||||
icon?: IconProps['name']
|
||||
label?: string
|
||||
value?: string
|
||||
content?: string
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { AvatarFallbackProps, AvatarRootProps } from 'radix-vue'
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/avatar'
|
||||
import type { IconProps } from '#ui/components/Icon.vue'
|
||||
|
||||
const appConfig = _appConfig as AppConfig & { ui: { avatar: Partial<typeof theme> } }
|
||||
|
||||
@@ -14,7 +15,7 @@ type AvatarVariants = VariantProps<typeof avatar>
|
||||
export interface AvatarProps extends Omit<AvatarRootProps, 'asChild'>, Omit<AvatarFallbackProps, 'as' | 'asChild'> {
|
||||
src?: string
|
||||
alt?: string
|
||||
icon?: string
|
||||
icon?: IconProps['name']
|
||||
text?: string
|
||||
size?: AvatarVariants['size']
|
||||
class?: any
|
||||
|
||||
117
src/runtime/components/Checkbox.vue
Normal file
117
src/runtime/components/Checkbox.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<script lang="ts">
|
||||
import { tv, type VariantProps } from 'tailwind-variants'
|
||||
import type { CheckboxRootProps, CheckboxRootEmits } from 'radix-vue'
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/checkbox'
|
||||
import type { IconProps } from '#ui/components/Icon.vue'
|
||||
|
||||
const appConfig = _appConfig as AppConfig & { ui: { checkbox: Partial<typeof theme> } }
|
||||
|
||||
const checkbox = tv({ extend: tv(theme), ...(appConfig.ui?.checkbox || {}) })
|
||||
|
||||
type CheckboxVariants = VariantProps<typeof checkbox>
|
||||
|
||||
export interface CheckboxProps extends Omit<CheckboxRootProps, 'asChild'> {
|
||||
id?: string
|
||||
name?: string
|
||||
description?: string
|
||||
label?: string
|
||||
color?: CheckboxVariants['color']
|
||||
size?: CheckboxVariants['size']
|
||||
icon?: IconProps['name']
|
||||
indeterminateIcon?: IconProps['name']
|
||||
indeterminate?: boolean
|
||||
class?: any
|
||||
ui?: Partial<typeof checkbox.slots>
|
||||
}
|
||||
|
||||
export interface CheckboxEmits extends CheckboxRootEmits {}
|
||||
|
||||
export interface CheckboxSlots {
|
||||
label(props: { label?: string }): any
|
||||
description(props: { description?: string }): any
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { CheckboxRoot, CheckboxIndicator, Label, useForwardPropsEmits } from 'radix-vue'
|
||||
import { reactivePick } from '@vueuse/core'
|
||||
import { useId } from '#imports'
|
||||
import { useFormField } from '#ui/composables/useFormField'
|
||||
import { useAppConfig } from '#app'
|
||||
|
||||
const props = defineProps<CheckboxProps>()
|
||||
const emits = defineEmits<CheckboxEmits>()
|
||||
defineSlots<CheckboxSlots>()
|
||||
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'defaultChecked', 'disabled', 'required', 'name'), emits)
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const { inputId: _inputId, emitFormChange, size, color, name, disabled } = useFormField<CheckboxProps>(props)
|
||||
const inputId = _inputId.value ?? useId()
|
||||
|
||||
const modelValue = defineModel<boolean | undefined>({
|
||||
default: undefined,
|
||||
set (value) {
|
||||
return value
|
||||
}
|
||||
})
|
||||
|
||||
const indeterminate = computed(() => (modelValue.value === undefined && props.indeterminate))
|
||||
|
||||
const checked = computed({
|
||||
get () {
|
||||
return indeterminate.value ? 'indeterminate' : modelValue.value
|
||||
},
|
||||
set (value) {
|
||||
modelValue.value = value === 'indeterminate' ? undefined : value
|
||||
}
|
||||
})
|
||||
|
||||
function onChecked () {
|
||||
emitFormChange()
|
||||
}
|
||||
|
||||
const ui = computed(() => tv({ extend: checkbox, slots: props.ui })({
|
||||
size: size.value,
|
||||
color: color.value,
|
||||
required: props.required,
|
||||
disabled: disabled.value,
|
||||
checked: modelValue.value ?? props.defaultChecked,
|
||||
indeterminate: indeterminate.value
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="ui.root({ class: props.class })">
|
||||
<div :class="ui.container()">
|
||||
<CheckboxRoot
|
||||
:id="inputId"
|
||||
v-model:checked="checked"
|
||||
v-bind="{ ...rootProps, name, disabled }"
|
||||
:class="ui.base()"
|
||||
@update:checked="onChecked"
|
||||
>
|
||||
<CheckboxIndicator :class="ui.indicator()">
|
||||
<UIcon v-if="indeterminate" :name="indeterminateIcon || appConfig.ui.icons.minus" :class="ui.icon()" />
|
||||
<UIcon v-else :name="icon || appConfig.ui.icons.check" :class="ui.icon()" />
|
||||
</CheckboxIndicator>
|
||||
</CheckboxRoot>
|
||||
</div>
|
||||
|
||||
<div v-if="(label || $slots.label) || (description || $slots.description)" :class="ui.wrapper()">
|
||||
<Label v-if="label || $slots.label" :for="inputId" :class="ui.label()">
|
||||
<slot name="label" :label="label">
|
||||
{{ label }}
|
||||
</slot>
|
||||
</Label>
|
||||
<p v-if="description || $slots.description" :class="ui.description()">
|
||||
<slot name="description" :description="description">
|
||||
{{ description }}
|
||||
</slot>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -3,8 +3,8 @@ import { tv } from 'tailwind-variants'
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/form'
|
||||
import { getYupErrors, isYupSchema, getValibotError, isValibotSchema, getZodErrors, isZodSchema, getJoiErrors, isJoiSchema } from '../utils/form'
|
||||
import type { FormSchema, FormError, FormInputEvents, FormErrorEvent, FormSubmitEvent, FormEvent, FormInjectedOptions, Form, FormErrorWithId } from '../types/form'
|
||||
import { getYupErrors, isYupSchema, getValibotError, isValibotSchema, getZodErrors, isZodSchema, getJoiErrors, isJoiSchema } from '#ui/utils/form'
|
||||
import type { FormSchema, FormError, FormInputEvents, FormErrorEvent, FormSubmitEvent, FormEvent, FormInjectedOptions, Form, FormErrorWithId } from '#ui/types/form'
|
||||
|
||||
const appConfig = _appConfig as AppConfig & { ui: { form: Partial<typeof theme> } }
|
||||
|
||||
|
||||
@@ -37,7 +37,8 @@ export interface FormFieldSlots {
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, inject, provide, type Ref } from 'vue'
|
||||
import type { FormError, FormFieldInjectedOptions } from '../types/form'
|
||||
import { Label } from 'radix-vue'
|
||||
import type { FormError, FormFieldInjectedOptions } from '#ui/types/form'
|
||||
import { useId } from '#imports'
|
||||
|
||||
const props = defineProps<FormFieldProps>()
|
||||
@@ -73,11 +74,11 @@ provide<FormFieldInjectedOptions<FormFieldProps>>('form-field', {
|
||||
<div :class="ui.root({ class: props.class })">
|
||||
<div :class="ui.wrapper()">
|
||||
<div v-if="label || $slots.label" :class="ui.labelWrapper()">
|
||||
<label :for="inputId" :class="ui.label()">
|
||||
<Label :for="inputId" :class="ui.label()">
|
||||
<slot name="label" :label="label">
|
||||
{{ label }}
|
||||
</slot>
|
||||
</label>
|
||||
</Label>
|
||||
<span v-if="hint || $slots.hint" :class="ui.hint()">
|
||||
<slot name="hint" :hint="hint">
|
||||
{{ hint }}
|
||||
@@ -95,38 +96,16 @@ provide<FormFieldInjectedOptions<FormFieldProps>>('form-field', {
|
||||
<div :class="label ? ui.container() : ''">
|
||||
<slot :error="error" />
|
||||
|
||||
<Transition name="slide-fade" mode="out-in">
|
||||
<p v-if="(typeof error === 'string' && error) || $slots.error" :class="ui.error()">
|
||||
<slot name="error" :error="error">
|
||||
{{ error }}
|
||||
</slot>
|
||||
</p>
|
||||
<p v-else-if="help || $slots.help" :class="ui.help()">
|
||||
<slot name="help" :help="help">
|
||||
{{ help }}
|
||||
</slot>
|
||||
</p>
|
||||
</Transition>
|
||||
<p v-if="(typeof error === 'string' && error) || $slots.error" :class="ui.error()">
|
||||
<slot name="error" :error="error">
|
||||
{{ error }}
|
||||
</slot>
|
||||
</p>
|
||||
<p v-else-if="help || $slots.help" :class="ui.help()">
|
||||
<slot name="help" :help="help">
|
||||
{{ help }}
|
||||
</slot>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.slide-fade-enter-active {
|
||||
transition: all 0.15s ease-out;
|
||||
}
|
||||
|
||||
.slide-fade-leave-active {
|
||||
transition: all 0.15s ease-out;
|
||||
}
|
||||
|
||||
.slide-fade-enter-from {
|
||||
transform: translateY(-10px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-fade-leave-to {
|
||||
transform: translateY(10px);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,6 +7,7 @@ import theme from '#build/ui/navigationMenu'
|
||||
import type { LinkProps } from '#ui/components/Link.vue'
|
||||
import type { AvatarProps } from '#ui/components/Avatar.vue'
|
||||
import type { BadgeProps } from '#ui/components/Badge.vue'
|
||||
import type { IconProps } from '#ui/components/Icon.vue'
|
||||
|
||||
const appConfig = _appConfig as AppConfig & { ui: { navigationMenu: Partial<typeof theme> } }
|
||||
|
||||
@@ -14,7 +15,7 @@ const navigationMenu = tv({ extend: tv(theme), ...(appConfig.ui?.navigationMenu
|
||||
|
||||
export interface NavigationMenuLink extends LinkProps {
|
||||
label: string | number
|
||||
icon?: string
|
||||
icon?: IconProps['name']
|
||||
avatar?: AvatarProps
|
||||
badge?: string | number | BadgeProps
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { SwitchRootProps, SwitchRootEmits } from 'radix-vue'
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/switch'
|
||||
import type { IconProps } from '#ui/components/Icon.vue'
|
||||
|
||||
const appConfig = _appConfig as AppConfig & { ui: { switch: Partial<typeof theme> } }
|
||||
|
||||
@@ -15,9 +16,9 @@ export interface SwitchProps extends Omit<SwitchRootProps, 'asChild'> {
|
||||
color?: SwitchVariants['color']
|
||||
size?: SwitchVariants['size']
|
||||
loading?: boolean
|
||||
loadingIcon?: string
|
||||
checkedIcon?: string
|
||||
uncheckedIcon?: string
|
||||
loadingIcon?: IconProps['name']
|
||||
checkedIcon?: IconProps['name']
|
||||
uncheckedIcon?: IconProps['name']
|
||||
class?: any
|
||||
ui?: Partial<typeof switchTv.slots>
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { computed } from 'vue'
|
||||
import { useAppConfig } from '#app'
|
||||
import type { IconProps } from '#ui/components/Icon.vue'
|
||||
|
||||
export interface UseComponentIconsProps {
|
||||
icon?: string
|
||||
icon?: IconProps['name']
|
||||
leading?: boolean
|
||||
leadingIcon?: string
|
||||
leadingIcon?: IconProps['name']
|
||||
trailing?: boolean
|
||||
trailingIcon?: string
|
||||
trailingIcon?: IconProps['name']
|
||||
loading?: boolean
|
||||
loadingIcon?: string
|
||||
loadingIcon?: IconProps['name']
|
||||
}
|
||||
|
||||
export function useComponentIcons (props: UseComponentIconsProps) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { inject, ref, computed } from 'vue'
|
||||
import { type UseEventBusReturn, useDebounceFn } from '@vueuse/core'
|
||||
import type { FormEvent, FormInputEvents, FormFieldInjectedOptions, FormInjectedOptions } from '../types/form'
|
||||
import type { FormEvent, FormInputEvents, FormFieldInjectedOptions, FormInjectedOptions } from '#ui/types/form'
|
||||
|
||||
type Props<T> = {
|
||||
id?: string
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ZodSchema } from 'zod'
|
||||
import type { ValidationError as JoiError, Schema as JoiSchema } from 'joi'
|
||||
import type { ObjectSchema as YupObjectSchema, ValidationError as YupError } from 'yup'
|
||||
import type { ObjectSchemaAsync as ValibotObjectSchema } from 'valibot'
|
||||
import type { FormError } from '../types/form'
|
||||
import type { FormError } from '#ui/types/form'
|
||||
|
||||
export function isYupSchema (schema: any): schema is YupObjectSchema<any> {
|
||||
return schema.validate && schema.__isYupSchema__
|
||||
|
||||
@@ -8,40 +8,31 @@ export default {
|
||||
variants: {
|
||||
size: {
|
||||
'3xs': {
|
||||
root: 'size-4 text-[8px]',
|
||||
icon: 'size-2'
|
||||
root: 'size-4 text-[8px]'
|
||||
},
|
||||
'2xs': {
|
||||
root: 'size-5 text-[10px]',
|
||||
icon: 'size-2.5'
|
||||
root: 'size-5 text-[10px]'
|
||||
},
|
||||
xs: {
|
||||
root: 'size-6 text-xs',
|
||||
icon: 'size-3'
|
||||
root: 'size-6 text-xs'
|
||||
},
|
||||
sm: {
|
||||
root: 'size-7 text-sm',
|
||||
icon: 'size-3.5'
|
||||
root: 'size-7 text-sm'
|
||||
},
|
||||
md: {
|
||||
root: 'size-8 text-base',
|
||||
icon: 'size-4'
|
||||
root: 'size-8 text-base'
|
||||
},
|
||||
lg: {
|
||||
root: 'size-9 text-lg',
|
||||
icon: 'size-4.5'
|
||||
root: 'size-9 text-lg'
|
||||
},
|
||||
xl: {
|
||||
root: 'size-10 text-xl',
|
||||
icon: 'size-5'
|
||||
root: 'size-10 text-xl'
|
||||
},
|
||||
'2xl': {
|
||||
root: 'size-11 text-[22px]',
|
||||
icon: 'size-5.5'
|
||||
root: 'size-11 text-[22px]'
|
||||
},
|
||||
'3xl': {
|
||||
root: 'size-12 text-2xl',
|
||||
icon: 'size-6'
|
||||
root: 'size-12 text-2xl'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
79
src/theme/checkbox.ts
Normal file
79
src/theme/checkbox.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
export default (config: { colors: string[] }) => ({
|
||||
slots: {
|
||||
root: 'relative flex items-start',
|
||||
base: 'shrink-0 text-white dark:text-gray-900 rounded ring ring-inset ring-gray-300 dark:ring-gray-700 focus:outline-none focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900',
|
||||
container: 'flex items-center',
|
||||
wrapper: 'ms-2',
|
||||
indicator: 'flex',
|
||||
icon: 'size-full',
|
||||
label: 'font-medium text-gray-700 dark:text-gray-200',
|
||||
description: 'text-gray-500 dark:text-gray-400'
|
||||
},
|
||||
variants: {
|
||||
color: Object.fromEntries(config.colors.map((color: string) => [color, `focus-visible:ring-${color}-500 dark:focus-visible:ring-${color}-400`])),
|
||||
size: {
|
||||
'2xs': {
|
||||
base: 'size-3',
|
||||
container: 'h-4',
|
||||
wrapper: 'text-xs'
|
||||
},
|
||||
xs: {
|
||||
base: 'size-3.5',
|
||||
container: 'h-4',
|
||||
wrapper: 'text-xs'
|
||||
},
|
||||
sm: {
|
||||
base: 'size-4',
|
||||
container: 'h-5',
|
||||
wrapper: 'text-sm'
|
||||
},
|
||||
md: {
|
||||
base: 'size-[18px]',
|
||||
container: 'h-5',
|
||||
wrapper: 'text-sm'
|
||||
},
|
||||
lg: {
|
||||
base: 'size-5',
|
||||
container: 'h-6',
|
||||
wrapper: 'text-base'
|
||||
},
|
||||
xl: {
|
||||
base: 'size-[22px]',
|
||||
container: 'h-6',
|
||||
wrapper: 'text-base'
|
||||
}
|
||||
},
|
||||
required: {
|
||||
true: {
|
||||
label: 'after:content-[\'*\'] after:ms-0.5 after:text-red-500 dark:after:text-red-400'
|
||||
}
|
||||
},
|
||||
disabled: {
|
||||
true: {
|
||||
base: 'cursor-not-allowed opacity-75',
|
||||
label: 'cursor-not-allowed opacity-75',
|
||||
description: 'cursor-not-allowed opacity-75'
|
||||
}
|
||||
},
|
||||
checked: {
|
||||
true: ''
|
||||
},
|
||||
indeterminate: {
|
||||
true: ''
|
||||
}
|
||||
},
|
||||
compoundVariants: config.colors.flatMap((color) => ([{
|
||||
color,
|
||||
checked: true,
|
||||
class: `ring-2 ring-inset ring-${color}-500 dark:ring-${color}-400 bg-${color}-500 dark:bg-${color}-400`
|
||||
}, {
|
||||
color,
|
||||
indeterminate: true,
|
||||
class: `ring-2 ring-inset ring-${color}-500 dark:ring-${color}-400 bg-${color}-500 dark:bg-${color}-400`
|
||||
}
|
||||
])),
|
||||
defaultVariants: {
|
||||
size: 'sm',
|
||||
color: 'primary'
|
||||
}
|
||||
})
|
||||
@@ -16,7 +16,7 @@ export default {
|
||||
xs: { root: 'text-xs' },
|
||||
sm: { root: 'text-sm' },
|
||||
md: { root: 'text-sm' },
|
||||
lg: { root: 'text-sm' },
|
||||
lg: { root: 'text-base' },
|
||||
xl: { root: 'text-base' }
|
||||
},
|
||||
required: {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
export default {
|
||||
chevronDown: 'i-heroicons-chevron-down-20-solid',
|
||||
chevronLeft: 'i-heroicons-chevron-left-20-solid',
|
||||
chevronRight: 'i-heroicons-chevron-right-20-solid',
|
||||
check: 'i-heroicons-check-20-solid',
|
||||
close: 'i-heroicons-x-mark-20-solid',
|
||||
empty: 'i-heroicons-circle-stack-20-solid',
|
||||
loading: 'i-heroicons-arrow-path-20-solid',
|
||||
search: 'i-heroicons-magnifying-glass-20-solid',
|
||||
chevronDown: 'i-heroicons-chevron-down-20-solid',
|
||||
chevronLeft: 'i-heroicons-chevron-left-20-solid',
|
||||
chevronRight: 'i-heroicons-chevron-right-20-solid'
|
||||
minus: 'i-heroicons-minus-20-solid',
|
||||
search: 'i-heroicons-magnifying-glass-20-solid'
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ export { default as avatar } from './avatar'
|
||||
export { default as badge } from './badge'
|
||||
export { default as button } from './button'
|
||||
export { default as card } from './card'
|
||||
export { default as checkbox } from './checkbox'
|
||||
export { default as chip } from './chip'
|
||||
export { default as collapsible } from './collapsible'
|
||||
export { default as container } from './container'
|
||||
|
||||
@@ -10,10 +10,15 @@ export default (config: { colors: string[] }) => ({
|
||||
icon: `group-data-[state=checked]:text-${color}-500 dark:group-data-[state=checked]:text-${color}-400`
|
||||
}])),
|
||||
size: {
|
||||
'2xs': {
|
||||
root: 'h-3 w-5',
|
||||
thumb: 'size-2 data-[state=checked]:translate-x-2',
|
||||
icon: 'size-1'
|
||||
},
|
||||
xs: {
|
||||
root: 'h-4 w-7',
|
||||
thumb: 'size-3 data-[state=checked]:translate-x-3',
|
||||
icon: 'size-2.5'
|
||||
icon: 'size-2'
|
||||
},
|
||||
sm: {
|
||||
root: 'h-5 w-9',
|
||||
@@ -26,9 +31,14 @@ export default (config: { colors: string[] }) => ({
|
||||
icon: 'size-4'
|
||||
},
|
||||
lg: {
|
||||
root: 'h-7 w-[3.25rem]',
|
||||
root: 'h-7 w-[52px]',
|
||||
thumb: 'size-6 data-[state=checked]:translate-x-6',
|
||||
icon: 'size-5'
|
||||
},
|
||||
xl: {
|
||||
root: 'h-8 w-[60px]',
|
||||
thumb: 'size-7 data-[state=checked]:translate-x-7',
|
||||
icon: 'size-6'
|
||||
}
|
||||
},
|
||||
checked: {
|
||||
|
||||
Reference in New Issue
Block a user