feat(toggle): new component

This commit is contained in:
Timothy Yin 2024-11-25 22:35:39 +08:00
parent 177c4cfd38
commit 2729812a3c
7 changed files with 243 additions and 0 deletions

View File

@ -0,0 +1,69 @@
---
description: Get a dynamic switch component
since: 1.3.4
---
## Usage
Use the `v-model` directive to make it reactive.
::ComponentPreview
---
privateProps:
v-model: checked
---
::
### Colors
The `color` prop affects the background color of the toggle.
::ComponentPreview
---
privateProps:
modelValue: true
props:
color: primary
---
::
### Sizes
The default size of the toggle is `md`.
::ComponentPreview
---
props:
size: md
---
::
### Rounded
You can make the toggle rounded by setting the `rounded` prop to `true`.
::ComponentPreview
---
props:
rounded: true
size: md
---
::
### Disabled
Disable it.
::ComponentPreview
---
privateProps:
modelValue: true
props:
disabled: true
---
::
## Config
::ComponentDefaults
::

View File

@ -0,0 +1,100 @@
<script lang="ts">
import { computed, defineComponent, toRef, type PropType } from 'vue'
import { twJoin, twMerge } from 'tailwind-merge'
import { toggle } from '../../ui.config'
import type { DeepPartial, Strategy, ToggleColor, ToggleSize } from '../../types'
import { useRayUI } from '#build/imports'
const config = toggle
export default defineComponent({
props: {
modelValue: {
type: Boolean as PropType<boolean | null>,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
rounded: {
type: Boolean,
default: false,
},
size: {
type: String as PropType<ToggleSize>,
default: config.default.size,
},
color: {
type: String as PropType<ToggleColor>,
default: config.default.color,
},
class: {
type: String,
default: '',
},
ui: {
type: Object as PropType<DeepPartial<typeof config> & { strategy?: Strategy }>,
default: () => ({}),
},
},
emits: [
'update:modelValue',
'change',
],
setup(props, { emit }) {
const { ui, attrs } = useRayUI('toggle', toRef(props, 'ui'), config)
const checked = computed({
get: () => props.modelValue,
set: (value: boolean) => {
emit('update:modelValue', value)
emit('change', value)
},
})
const toggleClass = computed(() => {
return twMerge(twJoin(
ui.value.base,
props.rounded ? 'rounded-full' : ui.value.rounded,
ui.value.size[props.size],
ui.value.ring.replaceAll('{color}', props.color),
checked.value ? ui.value.active.replaceAll('{color}', props.color) : ui.value.inactive,
), props.class)
})
const bulletClass = computed(() => {
return twJoin(
ui.value.bullet.base,
props.rounded ? 'rounded-full' : ui.value.bullet.rounded,
ui.value.bullet.shadow,
ui.value.bullet.size[props.size],
!props.disabled && ui.value.bullet.translate[props.size],
checked.value ? ui.value.bullet.active[props.size] : ui.value.bullet.inactive,
)
})
const handleClick = () => {
if (!props.disabled) {
checked.value = !checked.value
}
}
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
checked,
toggleClass,
bulletClass,
handleClick,
}
},
})
</script>
<template>
<button :class="toggleClass" :disabled="disabled" v-bind="attrs" @click="handleClick">
<span :class="bulletClass" />
</button>
</template>

View File

@ -2,5 +2,6 @@ export * from './button'
export * from './message'
export * from './input'
export * from './kbd'
export * from './toggle'
export * from './utils'

View File

@ -0,0 +1,9 @@
import type { AppConfig } from '@nuxt/schema'
import type { toggle } from '../ui.config'
import type { ExtractDeepKey } from './utils'
import type colors from '#ray-colors'
export type ToggleSize =
| keyof typeof toggle.size
| ExtractDeepKey<AppConfig, ['rayui', 'toggle', 'size']>
export type ToggleColor = (typeof colors)[number]

View File

@ -0,0 +1,53 @@
export default {
base: 'relative inline-flex flex-shrink-0 shadow-inner disabled:cursor-not-allowed disabled:opacity-50 focus:outline-none transition ease-in-out duration-200 group',
rounded: 'rounded-md',
ring: 'focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900',
active: 'bg-{color}-500 dark:bg-{color}-400',
inactive: 'bg-gray-100 dark:bg-gray-800',
size: {
'2xs': 'h-3 w-5',
'xs': 'h-3.5 w-6',
'sm': 'h-4 w-7',
'md': 'h-5 w-9',
'lg': 'h-6 w-11',
'xl': 'h-7 w-[3.25rem]',
'2xl': 'h-8 w-[3.75rem]',
},
bullet: {
base: 'relative inline-block m-0.5 bg-white dark:bg-gray-900 pointer-events-none transform transition ease-in-out duration-300 group-active:scale-90 group-disabled:scale-100',
shadow: 'shadow',
rounded: 'rounded',
size: {
'2xs': 'h-2 w-2',
'xs': 'h-2.5 w-2.5',
'sm': 'h-3 w-3',
'md': 'h-4 w-4',
'lg': 'h-5 w-5',
'xl': 'h-6 w-6',
'2xl': 'h-7 w-7',
},
translate: {
'2xs': 'group-active:translate-x-1',
'xs': 'group-active:translate-x-1.5',
'sm': 'group-active:translate-x-2',
'md': 'group-active:translate-x-2.5',
'lg': 'group-active:translate-x-3',
'xl': 'group-active:translate-x-3.5',
'2xl': 'group-active:translate-x-4',
},
active: {
'2xs': 'translate-x-2',
'xs': 'translate-x-2.5',
'sm': 'translate-x-3',
'md': 'translate-x-4',
'lg': 'translate-x-5',
'xl': 'translate-x-6',
'2xl': 'translate-x-7',
},
inactive: 'translate-x-0',
},
default: {
size: 'md',
color: 'primary',
},
}

View File

@ -7,6 +7,7 @@ export { default as kbd } from './elements/kbd'
// forms
export { default as input } from './forms/input'
export { default as toggle } from './forms/toggle'
// overlays
export { default as message } from './overlays/message'

View File

@ -179,6 +179,16 @@ const safelistForComponent: Record<
variants: ['focus'],
},
],
toggle: colorsToRegex => [
{
pattern: RegExp(`^ring-(${colorsToRegex})-400$`),
variants: ['focus-visible', 'dark'],
},
{
pattern: RegExp(`^ring-(${colorsToRegex})-500$`),
variants: ['focus-visible'],
},
],
}
export const generateSafelist = (colors: string[], globalColors: string[]) => {