initial commit

This commit is contained in:
2024-02-26 09:23:09 +08:00
commit e0c96717c7
29 changed files with 7522 additions and 0 deletions

2
components/uni/Button/index.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
type ButtonType = 'normal' | 'primary' | 'danger'
type ButtonSize = 'base' | 'medium' | 'small'

View File

@@ -0,0 +1,123 @@
<script lang="ts" setup>
const emit = defineEmits(['click'])
const props = defineProps({
type: {
type: String as PropType<ButtonType>,
default: 'normal'
},
attrType: {
type: String as PropType<'button' | 'submit' | 'reset'>,
default: 'button'
},
size: {
type: String as PropType<ButtonSize>,
default: 'base'
},
block: {
type: Boolean,
default: false
},
icon: {
type: String,
default: ''
},
loading: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
}
})
const buttonTypeClass = computed(() => {
let ret = `uni-button--normal`
if (props.type !== 'normal') ret += ` uni-button--${props.type}`
return ret
})
const buttonSizeClass = computed(() => {
return `uni-button--${props.size}`
})
const buttonIcon = computed(() => {
if (props.icon) return props.icon
return null
})
const handleClick = (e: any) => {
emit('click', e)
}
</script>
<template>
<button class="w-fit flex justify-center items-center rounded-md font-bold border shadow-sm transition focus:ring-4"
:class="{
'w-full': block,
'uni-button--disabled': disabled || loading,
[buttonTypeClass]: buttonTypeClass,
[buttonSizeClass]: buttonSizeClass,
}" @click="handleClick" :disabled="disabled || loading" :type="attrType">
<Transition name="icon">
<UniIconSpinner v-if="loading" />
<Icon v-else-if="buttonIcon" :name="buttonIcon" :key="buttonIcon" />
<span v-else class="mr-2">
<slot name="icon"/>
</span>
</Transition>
<div class="flex items-center whitespace-nowrap leading-snug" :class="{ 'ml-2': buttonIcon || loading }">
<slot />
</div>
</button>
</template>
<style scoped>
.icon-enter-active,
.icon-leave-active {
transition: all .3s ease;
}
.icon-enter-from,
.icon-leave-to {
opacity: 0;
width: 0;
}
.uni-button--normal {
@apply bg-neutral-50 hover:bg-neutral-100 active:bg-neutral-200 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:active:bg-neutral-600;
@apply ring-neutral-200/50 dark:ring-neutral-800/50;
@apply border-neutral-300 dark:border-neutral-700;
@apply text-neutral-700 dark:text-neutral-300;
}
.uni-button--primary {
@apply text-blue-600 dark:text-blue-600;
}
.uni-button--danger {
@apply text-red-500 dark:text-red-500;
}
.uni-button--disabled {
@apply bg-neutral-100 dark:bg-neutral-900 hover:bg-neutral-100 hover:dark:bg-neutral-900;
@apply ring-transparent;
@apply border-transparent;
@apply text-neutral-400 dark:text-neutral-600;
}
.uni-button--base {
@apply text-base;
@apply px-4 py-2;
}
.uni-button--medium {
@apply text-sm;
@apply px-3 py-1.5;
}
.uni-button--small {
@apply text-sm;
@apply px-2 py-1;
}
</style>

View File

@@ -0,0 +1,82 @@
<script setup lang="ts">
import {useMessage} from "~/composables/uni/useMessage";
const props = defineProps({
hideIcon: {
type: Boolean,
default: false
},
iconSize: {
type: String,
default: '1em'
},
text: {
type: String,
required: true
}
})
const message = useMessage()
const copied = ref(false)
const copied_timeout = ref()
const fuck_copy = () => {
navigator.clipboard.writeText(props.text || '').then(() => {
copied.value = true
if (copied_timeout.value) clearInterval(copied_timeout.value)
copied_timeout.value = setTimeout(() => copied.value = false, 1500)
}).catch(e => {
message.error(`复制失败`)
})
}
</script>
<template>
<div class="inline-flex items-center gap-0.5 cursor-pointer" @click="fuck_copy">
<slot/>
<Transition v-if="!hideIcon" name="icon" mode="out-in">
<svg v-if="!copied" xmlns="http://www.w3.org/2000/svg" :width="iconSize" :height="iconSize" viewBox="0 0 24 24"
class="text-neutral-500">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M8 10a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-8a2 2 0 0 1-2-2z"/>
<path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"/>
</g>
</svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" :width="iconSize" :height="iconSize" viewBox="0 0 24 24"
class="text-green-600">
<defs>
<mask id="lineMdCheckAll0">
<g fill="none" stroke="#fff" stroke-dasharray="22" stroke-dashoffset="22" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2">
<path d="M2 13.5l4 4l10.75 -10.75">
<animate fill="freeze" attributeName="stroke-dashoffset" dur="0.2s" values="22;0"/>
</path>
<path stroke="#000" stroke-width="4" d="M7.5 13.5l4 4l10.75 -10.75" opacity="0">
<set attributeName="opacity" begin="0.2s" to="1"/>
<animate fill="freeze" attributeName="stroke-dashoffset" begin="0.2s" dur="0.2s" values="22;0"/>
</path>
<path d="M7.5 13.5l4 4l10.75 -10.75" opacity="0">
<set attributeName="opacity" begin="0.2s" to="1"/>
<animate fill="freeze" attributeName="stroke-dashoffset" begin="0.2s" dur="0.2s" values="22;0"/>
</path>
</g>
</mask>
</defs>
<rect width="24" height="24" fill="currentColor" mask="url(#lineMdCheckAll0)"/>
</svg>
</Transition>
</div>
</template>
<style scoped>
.icon-enter-active,
.icon-leave-active {
@apply transition duration-300;
}
.icon-enter-from,
.icon-leave-to {
@apply opacity-0;
}
</style>

View File

@@ -0,0 +1,10 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<g fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="M0 0h24v24H0z"></path>
<path fill="currentColor"
d="M17 3.34a10 10 0 1 1-14.995 8.984L2 12l.005-.324A10 10 0 0 1 17 3.34zm-6.489 5.8a1 1 0 0 0-1.218 1.567L10.585 12l-1.292 1.293l-.083.094a1 1 0 0 0 1.497 1.32L12 13.415l1.293 1.292l.094.083a1 1 0 0 0 1.32-1.497L13.415 12l1.292-1.293l.083-.094a1 1 0 0 0-1.497-1.32L12 10.585l-1.293-1.292l-.094-.083z">
</path>
</g>
</svg>
</template>

View File

@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path fill="currentColor" fillRule="evenodd"
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2s10 4.477 10 10Zm-10 5.75a.75.75 0 0 0 .75-.75v-6a.75.75 0 0 0-1.5 0v6c0 .414.336.75.75.75ZM12 7a1 1 0 1 1 0 2a1 1 0 0 1 0-2Z"
clipRule="evenodd"></path>
</svg>
</template>

View File

@@ -0,0 +1,10 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<g fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="M0 0h24v24H0z"></path>
<path fill="currentColor"
d="M17 3.34a10 10 0 1 1-14.995 8.984L2 12l.005-.324A10 10 0 0 1 17 3.34zm-1.293 5.953a1 1 0 0 0-1.32-.083l-.094.083L11 12.585l-1.293-1.292l-.094-.083a1 1 0 0 0-1.403 1.403l.083.094l2 2l.094.083a1 1 0 0 0 1.226 0l.094-.083l4-4l.083-.094a1 1 0 0 0-.083-1.32z">
</path>
</g>
</svg>
</template>

View File

@@ -0,0 +1,10 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<g fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<path d="M0 0h24v24H0z"></path>
<path fill="currentColor"
d="M12 2c5.523 0 10 4.477 10 10a10 10 0 0 1-19.995.324L2 12l.004-.28C2.152 6.327 6.57 2 12 2zm.01 13l-.127.007a1 1 0 0 0 0 1.986L12 17l.127-.007a1 1 0 0 0 0-1.986L12.01 15zM12 7a1 1 0 0 0-.993.883L11 8v4l.007.117a1 1 0 0 0 1.986 0L13 12V8l-.007-.117A1 1 0 0 0 12 7z">
</path>
</g>
</svg>
</template>

View File

@@ -0,0 +1,11 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"></path>
<path fill="currentColor"
d="M12,4a8,8,0,0,1,7.89,6.7A1.53,1.53,0,0,0,21.38,12h0a1.5,1.5,0,0,0,1.48-1.75,11,11,0,0,0-21.72,0A1.5,1.5,0,0,0,2.62,12h0a1.53,1.53,0,0,0,1.49-1.3A8,8,0,0,1,12,4Z">
<animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate"
values="0 12 12;360 12 12"></animateTransform>
</path>
</svg>
</template>

View File

@@ -0,0 +1,90 @@
<script lang="ts" setup>
import type {PropType} from "vue";
const emit = defineEmits(['input', 'change', 'update:modelValue'])
const props = defineProps({
label: {
type: String,
required: false,
default: ''
},
modelValue: {
type: [String, Number] as PropType<string | number | undefined>,
required: true
},
placeholder: {
type: String,
required: false,
default: ''
},
type: {
type: String as PropType<'text' | 'password' | 'number' | 'email' | 'tel' | 'date'>,
required: false,
default: 'text'
},
justify: {
type: String as PropType<'start' | 'end'>,
required: false,
default: 'end'
},
pattern: {
type: [String, RegExp],
required: false,
},
disabled: {
type: Boolean,
required: false,
default: false
},
})
const inputValue = ref(props.modelValue)
const isError = ref(false)
watch(() => props.modelValue, (value) => {
inputValue.value = value
if (props.pattern && value) {
const pattern = typeof props.pattern === 'string' ? new RegExp(props.pattern) : props.pattern
isError.value = !pattern.test(value as string)
pattern.lastIndex = 0
}
}, { immediate: true })
const handleInput = (e: any) => {
if (props.disabled) return
const value = e.target.value
if (props.pattern && value && props.type !== 'date') {
const pattern = typeof props.pattern === 'string' ? new RegExp(props.pattern) : props.pattern
isError.value = !pattern.test(value)
pattern.lastIndex = 0
inputValue.value = value
if (isError.value) return
}
inputValue.value = value
isError.value = false
emit('update:modelValue', e.target.value)
emit('input', e)
}
</script>
<template>
<div class="flex flex-col space-y-1"
:class="{ 'justify-start': justify === 'start', 'justify-end': justify === 'end' }">
<p class="block w-fit text-neutral-700 dark:text-neutral-300 text-sm font-bold font-['Nunito']" v-if="label">
{{ label }}
</p>
<div class="relative">
<input class="relative w-full flex items-center gap-2.5 p-2 pr-2 rounded-md overflow-hidden border transition bg-white dark:bg-neutral-800
border-neutral-200 dark:border-neutral-800 focus:border-neutral-400 dark:focus:border-neutral-700
focus:ring-4 focus:ring-opacity-50 focus:ring-neutral-200 dark:focus:ring-neutral-800
outline-none placeholder-neutral-400 dark:placeholder-neutral-500 shadow-sm"
:class="{ '!border-red-500': isError, 'bg-neutral-100 dark:bg-neutral-900 text-neutral-400 dark:text-neutral-600': disabled }"
:value="inputValue" @input="handleInput" :placeholder="placeholder" :disabled="disabled" :type="type"/>
</div>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,98 @@
<script lang="ts" setup>
import type {Message, MessageApi, MessageProviderApi, MessageType} from "~/components/uni/Message/index";
const props = defineProps({
max: {
type: Number,
default: 5
}
})
const nuxtApp = useNuxtApp()
const messageList = ref<Message[]>([])
const createMessage = (content: string, type: MessageType, duration: number = 3000) => {
const {max} = props
messageList.value.push({
id: (Date.now() + Math.random() * 100).toString(32).toUpperCase(),
content,
type,
duration
})
if (messageList.value.length > max) {
messageList.value.shift()
}
}
const providerApi: MessageProviderApi = {
destroy: (id: string) => {
messageList.value.splice(messageList.value.findIndex(message => message.id === id), 1)
}
}
const api: MessageApi = {
info: (content: string, duration: number = 3000) => {
createMessage(content, 'info', duration);
},
success: (content: string, duration: number = 3000) => {
createMessage(content, 'success', duration);
},
warning: (content: string, duration: number = 3000) => {
createMessage(content, 'warning', duration);
},
error: (content: string, duration: number = 3000) => {
createMessage(content, 'error', duration);
},
destroyAll: function (): void {
throw new Error('Function not implemented.');
}
}
nuxtApp.vueApp.provide('uni-message-provider', providerApi)
nuxtApp.vueApp.provide('uni-message', api)
</script>
<template>
<slot/>
<teleport to="body">
<div id="message-provider">
<div class="message-wrapper">
<TransitionGroup name="message">
<UniMessage v-for="(message, k) in messageList" :key="message.id" :message="message"/>
</TransitionGroup>
</div>
</div>
</teleport>
</template>
<style scoped>
#message-provider .message-wrapper {
@apply z-[50000] fixed inset-0 flex flex-col items-center pointer-events-none;
}
.message-move,
.message-leave-active {
transition: all .6s ease;
}
.message-enter-active {
transition: all .6s cubic-bezier(0.075, 0.82, 0.165, 1);
}
.message-enter-from {
filter: blur(2px);
opacity: 0;
transform: translateY(-100%);
}
.message-leave-to {
filter: blur(6px);
opacity: 0;
transform: translateY(-20%);
}
.message-leave-active {
position: absolute;
}
</style>

20
components/uni/Message/index.d.ts vendored Normal file
View File

@@ -0,0 +1,20 @@
export type Message = {
id: string
content: string
type: MessageType
duration?: number
}
export type MessageType = 'success' | 'warning' | 'error' | 'info'
export type MessageProviderApi = {
destroy: (id: string) => void
}
export type MessageApi = {
info: (content: string, duration?: number) => void
success: (content: string, duration?: number) => void
warning: (content: string, duration?: number) => void
error: (content: string, duration?: number) => void
destroyAll: () => void
}

View File

@@ -0,0 +1,63 @@
<script lang="ts" setup>
import type {Message, MessageProviderApi} from "~/components/uni/Message/index";
const providerApi = inject<MessageProviderApi>('uni-message-provider')
const props = defineProps({
message: {
require: true,
type: Object
}
})
const message = ref<Message>(props.message as Message)
onMounted(() => {
setTimeout(() => {
providerApi?.destroy(message.value.id)
}, message.value?.duration || 3000);
})
</script>
<template>
<div class="message" :class="{
'!text-blue-500 !border-blue-400 !bg-blue-50': message.type === 'info',
'!text-emerald-500 !border-emerald-400 !bg-emerald-50': message.type === 'success',
'!text-orange-500 !border-orange-400 !bg-orange-50': message.type === 'warning',
'!text-rose-500 !border-rose-400 !bg-rose-50': message.type === 'error',
[message.type]: message.type
}">
<UniIconCircleSuccess v-if="message.type === 'success'" class="text-xl" />
<UniIconCircleWarning v-if="message.type === 'warning'" class="text-xl" />
<UniIconCircleError v-if="message.type === 'error'" class="text-xl" />
<UniIconCircleInfo v-if="message.type === 'info'" class="text-xl" />
<span>
{{ message.content }}
</span>
</div>
</template>
<style scoped>
.message {
min-width: 80px;
box-shadow: 0 4px 12px rgba(0, 0, 0, .2);
@apply h-fit px-2 py-1.5 border bg-white border-gray-300 rounded-md text-gray-500 text-xs flex items-center gap-1.5 first-of-type:mt-2.5 mt-2.5 font-bold pointer-events-auto;
}
.message.info {
box-shadow: 0 4px 12px rgba(59, 130, 246, .2);
}
.message.success {
box-shadow: 0 4px 12px rgba(16, 185, 129, .2);
}
.message.warning {
box-shadow: 0 4px 12px rgba(249, 115, 22, .2);
}
.message.error {
box-shadow: 0 4px 12px rgba(244, 63, 94, .2);
}
</style>

6
components/uni/Select/index.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
type SelectItem = {
label: string
value: string
icon?: string
disabled?: boolean
}

View File

@@ -0,0 +1,129 @@
<script lang="ts" setup>
import {computed, type PropType} from "vue";
const emit = defineEmits(['input', 'change', 'update:modelValue'])
const props = defineProps({
label: {
type: String,
required: false,
default: ''
},
modelValue: {
type: [String, Number],
required: false
},
items: {
type: Array as PropType<SelectItem[]>,
required: true
},
justify: {
type: String as PropType<'start' | 'end'>,
required: false,
default: 'end'
},
disabled: {
type: Boolean,
required: false,
default: false
},
align: {
type: String as PropType<'bottom' | 'top'>,
required: false,
default: 'bottom'
}
})
const selectWrapperRef = ref()
const selectRef = ref()
const optionsRef = ref()
const optionsAlign = computed(() => {
switch (props.align) {
case "bottom":
return 'top-full mt-2'
case "top":
return 'bottom-full mb-2'
}
})
const hasAnyIcon = computed(() => props.items.some(item => item.icon))
const selectedItem = computed(() => props.items.find(item => item.value === props.modelValue) as SelectItem)
const optionsExpanded = ref(false)
const selectedIconFlag = ref(true)
const handleSelectClick = () => {
optionsExpanded.value = !optionsExpanded.value
}
const handleOptionSelect = (option: SelectItem) => {
emit('input', option.value)
emit('change', option.value)
emit('update:modelValue', option.value)
selectedIconFlag.value = false;
nextTick(() => {
selectedIconFlag.value = true;
});
}
onMounted(() => {
selectRef.value.ownerDocument.addEventListener('click', (e: { target: any; }) => {
if (optionsExpanded && !selectRef?.value?.contains(e.target)) {
optionsExpanded.value = false
}
})
})
</script>
<template>
<div class="flex flex-col space-y-1"
:class="{ 'justify-start': justify === 'start', 'justify-end': justify === 'end' }">
<p class="block w-fit text-neutral-700 dark:text-neutral-300 text-sm font-bold font-['Nunito']" v-if="label">
{{ label }}
</p>
<div class="relative" ref="selectWrapperRef">
<button class="relative w-full flex items-center gap-2.5 p-2 pr-6 rounded-md overflow-hidden border transition bg-white dark:bg-neutral-800
border-neutral-200 dark:border-neutral-800 focus:border-neutral-400 dark:focus:border-neutral-700
focus:ring-4 focus:ring-opacity-50 focus:ring-neutral-200 dark:focus:ring-neutral-800 shadow-sm"
:class="{ 'cursor-not-allowed bg-neutral-100 dark:bg-neutral-900 text-neutral-400 dark:text-neutral-600': disabled }" ref="selectRef" type="button" @click="handleSelectClick" :disabled="disabled">
<span v-if="selectedItem?.icon && !selectedIconFlag && hasAnyIcon"
class="inline-block w-5 h-5 pointer-events-none"></span>
<Icon v-else-if="selectedItem?.icon && selectedIconFlag && hasAnyIcon" :name="selectedItem?.icon"
class="inline-block w-5 h-5 pointer-events-none" />
<Transition name="select-item" mode="out-in">
<span class="leading-snug whitespace-nowrap text-sm" :key="selectedItem?.value">{{
selectedItem?.label || selectedItem?.value || 'Select an option'
}}</span>
</Transition>
<Icon name="tabler:dots-vertical"
class="absolute bg-neutral-50 text-gray-500 dark:bg-neutral-700/50 dark:text-neutral-500 inset-y-0 right-0 h-full" />
</button>
<div class="absolute right-0 w-full md:w-fit rounded-md border overflow-x-hidden overflow-y-auto transition shadow-lg opacity-0
bg-white dark:bg-neutral-800 border-neutral-200 dark:border-neutral-800 z-50 max-h-64"
:class="{ 'opacity-100 pointer-events-auto': optionsExpanded, '-translate-y-4 pointer-events-none': !optionsExpanded, [optionsAlign]: optionsAlign }"
ref="optionsRef">
<div class="flex items-center gap-2.5 px-2 py-2 cursor-pointer
dark:text-neutral-300 font-['Nunito'] transition whitespace-nowrap
bg-white dark:bg-neutral-800 hover:bg-neutral-100 dark:hover:bg-neutral-700"
v-for="(option, index) in items" :key="index" :class="{
'!bg-neutral-200 dark:!bg-neutral-700 hover:!bg-neutral-200 dark:hover:!bg-neutral-700': option.value === selectedItem?.value,
'!cursor-not-allowed text-neutral-300 dark:text-neutral-500 hover:bg-white dark:hover:!bg-neutral-800': option.disabled
}" @click="!option.disabled ? handleOptionSelect(option) : void 0">
<div class="inline-block w-5 h-5" v-if="hasAnyIcon && !option.icon"></div>
<Icon :name="(option?.icon)" class="inline-block w-5 h-5" v-if="option.icon" />
<span class="leading-none whitespace-nowrap text-sm font-sans">{{ option.label || 'No label' }}</span>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.select-item-enter-active,
.select-item-leave-active {
transition: all .15s ease;
}
.select-item-enter-from,
.select-item-leave-to {
opacity: .5;
filter: blur(2px);
}
</style>

View File

@@ -0,0 +1,112 @@
import { textarea } from '@nuxt/ui';
<script lang="ts" setup>
const emit = defineEmits(['input', 'change', 'update:modelValue'])
const props = defineProps({
label: {
type: String,
required: false,
default: ''
},
modelValue: {
type: [String, Number],
required: true
},
placeholder: {
type: String,
required: false,
default: ''
},
justify: {
type: String as PropType<'start' | 'end'>,
required: false,
default: 'end'
},
pattern: {
type: [String, RegExp],
required: false,
},
disabled: {
type: Boolean,
required: false,
default: false
},
rows: {
type: Number,
required: false,
default: 5
},
minRows: {
type: Number,
required: false,
},
})
const textAreaRef = ref()
const inputValue = ref(props.modelValue)
const isError = ref(false)
watch(() => props.modelValue, (value) => {
inputValue.value = value
if (props.pattern && value) {
const pattern = typeof props.pattern === 'string' ? new RegExp(props.pattern) : props.pattern
isError.value = !pattern.test(value as string)
pattern.lastIndex = 0
}
}, { immediate: true })
const handleInput = (e: any) => {
if (props.disabled) return
const value = e.target.value
if (props.pattern && value) {
const pattern = typeof props.pattern === 'string' ? new RegExp(props.pattern) : props.pattern
isError.value = !pattern.test(value)
pattern.lastIndex = 0
inputValue.value = value
if (isError.value) return
}
inputValue.value = value
isError.value = false
emit('update:modelValue', e.target.value)
emit('input', e)
}
const autosize = (e: any) => {
const el = e?.target ? e.target : e
el.style.height = 'auto'
el.style.height = el.scrollHeight + 'px'
}
onMounted(() => {
if (props.minRows) {
const textarea = textAreaRef.value
textarea?.addEventListener('keydown', autosize)
textarea?.addEventListener('input', autosize)
textarea?.addEventListener('focus', autosize)
autosize(textarea)
}
})
</script>
<template>
<div class="flex flex-col space-y-1"
:class="{ 'justify-start': justify === 'start', 'justify-end': justify === 'end' }">
<p class="block w-fit text-neutral-700 dark:text-neutral-300 text-sm font-bold font-['Nunito']" v-if="label">
{{ label }}
</p>
<div class="relative">
<textarea class="relative w-full flex items-center gap-2.5 p-2 pr-6 rounded-md overflow-hidden overflow-y-auto border transition bg-white dark:bg-neutral-800
border-neutral-200 dark:border-neutral-800 focus:border-neutral-400 dark:focus:border-neutral-700
focus:ring-4 focus:ring-opacity-50 focus:ring-neutral-200 dark:focus:ring-neutral-800
outline-none placeholder-neutral-400 dark:placeholder-neutral-500 shadow-sm"
:rows="minRows || rows" ref="textAreaRef"
:class="{ '!border-red-500': isError, 'bg-neutral-100 dark:bg-neutral-900 text-neutral-400 dark:text-neutral-600': disabled }"
:value="inputValue" @input="handleInput" :placeholder="placeholder" :disabled="disabled" />
</div>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,133 @@
<script setup lang="ts">
import type {PropType} from "vue";
const emit = defineEmits(['input', 'change', 'update:modelValue'])
const props = defineProps({
modelValue: {
type: Boolean,
required: false
},
size: {
type: String as PropType<'sm' | 'md' | 'lg'>,
required: false,
default: 'md'
},
value: {
type: Boolean,
required: false
},
onIcon: {
type: String,
required: false,
},
offIcon: {
type: String,
required: false,
}
})
const checked = ref(false)
const buttonSizeClass = computed(() => {
switch (props.size) {
case 'sm':
return 'h-6 w-10'
case 'md':
return 'h-8 w-14'
case 'lg':
return 'h-10 w-[calc(2.5rem/0.54)]'
}
})
const buttonPaddingClass = computed(() => {
switch (props.size) {
case 'sm':
return 'p-1'
case 'md':
return 'p-1'
case 'lg':
return 'p-1.5'
}
})
const bulletSizeClass = computed(() => {
switch (props.size) {
case 'sm':
return 'h-4'
case 'md':
return 'h-6'
case 'lg':
return 'h-7'
}
})
const bulletTranslateClass = computed(() => {
switch (props.size) {
case 'sm':
return 'translate-x-4'
case 'md':
return 'translate-x-6'
case 'lg':
return 'translate-x-8'
}
})
const handleCheck = () => {
checked.value = !checked.value
emit('update:modelValue', checked.value)
emit('change', checked.value)
emit('input', checked.value)
}
onMounted(() => {
if (props.modelValue) {
checked.value = props.modelValue
} else if (props.value) {
checked.value = props.value
}
})
watch(() => props.modelValue, (value) => {
checked.value = value
})
</script>
<template>
<button
class="relative flex items-center rounded-lg bg-neutral-100 dark:bg-neutral-800 shadow-inner transition ease-in-out group outline-none"
:class="{
'!bg-green-400 dark:!bg-green-400/50': checked,
[buttonSizeClass]: buttonSizeClass,
[buttonPaddingClass]: buttonPaddingClass
}" @click="handleCheck">
<span
class="aspect-[1/1] translate-x-0 transition ease-in-out bg-white dark:bg-black rounded-md shadow duration-300 group-active:scale-90"
:class="{
'!shadow-lg': checked,
'group-active:translate-x-3 group-active:duration-500': !checked,
[bulletSizeClass]: bulletSizeClass,
[bulletTranslateClass]: checked
}">
<span v-if="onIcon || offIcon" class="absolute inset-0 flex items-center justify-center text-neutral-400">
<Transition name="icon" mode="out-in">
<slot v-if="checked" name="on-icon"/>
<slot v-else name="off-icon"/>
</Transition>
</span>
</span>
</button>
</template>
<style scoped>
.icon-enter-active,
.icon-leave-active {
transition: all 0.1s ease;
}
.icon-enter-from,
.icon-leave-to {
opacity: 0;
transform: scale(0.5);
}
</style>