mirror of
https://github.com/HoshinoSuzumi/rayine-ui.git
synced 2025-04-07 12:48:50 +08:00
✨ feat(button): add disabled and loading state
This commit is contained in:
parent
d913f91644
commit
ed419eff25
@ -3,7 +3,7 @@ title: Introduction
|
||||
description: Multi-purpose customizable components for RayineSoft projects
|
||||
---
|
||||
|
||||
Rayine UI is a collection of multi-purpose customizable components for RayineSoft projects.
|
||||
RayineUI is a multi-purpose customizable UI library for RayineSoft projects.
|
||||
|
||||
This project aims to facilitate sharing a component library across multiple projects for my own use. Open-sourcing it is just a bonus. Therefore, I am under no obligation to meet your requirements, and breaking changes may occur at any time. Of course, pull requests are welcome.
|
||||
|
||||
|
@ -12,6 +12,8 @@ Button
|
||||
|
||||
### Variants
|
||||
|
||||
#### soft
|
||||
|
||||
::ComponentPreview
|
||||
---
|
||||
props:
|
||||
@ -20,6 +22,36 @@ props:
|
||||
Button
|
||||
::
|
||||
|
||||
#### outline
|
||||
|
||||
::ComponentPreview
|
||||
---
|
||||
props:
|
||||
variant: outline
|
||||
---
|
||||
Button
|
||||
::
|
||||
|
||||
#### ghost
|
||||
|
||||
::ComponentPreview
|
||||
---
|
||||
props:
|
||||
variant: ghost
|
||||
---
|
||||
Button
|
||||
::
|
||||
|
||||
#### link
|
||||
|
||||
::ComponentPreview
|
||||
---
|
||||
props:
|
||||
variant: link
|
||||
---
|
||||
Button
|
||||
::
|
||||
|
||||
### Colors
|
||||
|
||||
::ComponentPreview
|
||||
@ -29,3 +61,23 @@ props:
|
||||
---
|
||||
Button
|
||||
::
|
||||
|
||||
### Disabled
|
||||
|
||||
::ComponentPreview
|
||||
---
|
||||
props:
|
||||
disabled: true
|
||||
---
|
||||
Button
|
||||
::
|
||||
|
||||
### Loading
|
||||
|
||||
::ComponentPreview
|
||||
---
|
||||
props:
|
||||
loading: true
|
||||
---
|
||||
Button
|
||||
::
|
||||
|
@ -20,10 +20,10 @@ const router = useRouter()
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center items-center gap-4">
|
||||
<RayButton @click="router.push('/getting-started')">
|
||||
<RayButton to="/getting-started">
|
||||
Getting Started
|
||||
</RayButton>
|
||||
<RayButton variant="outline" @click="router.push('/components')">
|
||||
<RayButton variant="outline" to="/components">
|
||||
Explore Components
|
||||
</RayButton>
|
||||
</div>
|
||||
|
@ -1,18 +1,27 @@
|
||||
<script lang="ts" setup>
|
||||
import { twJoin, twMerge } from 'tailwind-merge'
|
||||
import { computed, toRef, type PropType } from 'vue'
|
||||
import { button } from '../../ui.config'
|
||||
import type { DeepPartial, Strategy } from '../../types/utils'
|
||||
import type { ButtonColor, ButtonSize, ButtonVariant } from '../../types/button'
|
||||
import { getNonUndefinedValuesFromObject } from '../../utils'
|
||||
import { nuxtLinkProps } from '../../utils/link'
|
||||
import { button } from '../../ui.config'
|
||||
import { useRayUI } from '#build/imports'
|
||||
|
||||
const config = button
|
||||
|
||||
const props = defineProps({
|
||||
...nuxtLinkProps,
|
||||
class: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
padded: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
@ -25,6 +34,14 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
to: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
type: String as PropType<ButtonSize>,
|
||||
default: () => button.default.size,
|
||||
@ -37,13 +54,19 @@ const props = defineProps({
|
||||
type: String as PropType<ButtonVariant>,
|
||||
default: () => button.default.variant,
|
||||
},
|
||||
loadingIcon: {
|
||||
type: String,
|
||||
default: () => button.default.loadingIcon,
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<DeepPartial<typeof config> & { strategy?: Strategy }>,
|
||||
type: Object as PropType<DeepPartial<typeof button> & { strategy?: Strategy }>,
|
||||
default: () => ({}),
|
||||
},
|
||||
})
|
||||
|
||||
const { ui, attrs } = useRayUI('button', toRef(props, 'ui'), config)
|
||||
const extProps = computed(() => getNonUndefinedValuesFromObject(props, nuxtLinkProps))
|
||||
|
||||
const { ui, attrs } = useRayUI('button', toRef(props, 'ui'), button)
|
||||
|
||||
const buttonClass = computed(() => {
|
||||
// @ts-ignore
|
||||
@ -61,12 +84,14 @@ const buttonClass = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
:class="buttonClass"
|
||||
v-bind="{ ...attrs }"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
<RayLink type="button" :disabled="disabled || loading" :class="buttonClass" v-bind="{ ...extProps, ...attrs }">
|
||||
<slot name="leading" :disabled="disabled" :loading="loading">
|
||||
<IconSpinner v-if="loading" class="mr-1" />
|
||||
</slot>
|
||||
<slot>
|
||||
<span v-if="label">{{ label }}</span>
|
||||
</slot>
|
||||
</RayLink>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
65
src/runtime/components/elements/Link.vue
Normal file
65
src/runtime/components/elements/Link.vue
Normal file
@ -0,0 +1,65 @@
|
||||
<script lang="ts" setup>
|
||||
import { nuxtLinkProps } from '../../utils/link'
|
||||
|
||||
const props = defineProps({
|
||||
...nuxtLinkProps,
|
||||
to: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
as: {
|
||||
type: String,
|
||||
default: 'button',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'button',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
activeClass: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
inactiveClass: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="as"
|
||||
v-if="!to"
|
||||
:type="type"
|
||||
:class="active ? activeClass : inactiveClass"
|
||||
:disabled="disabled"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<slot v-bind="{ isActive: active }" />
|
||||
</component>
|
||||
<NuxtLink v-else v-slot="{ href, target, rel, navigate, isActive, isExternal }" v-bind="props" custom>
|
||||
<a
|
||||
v-bind="$attrs"
|
||||
:href="!disabled ? href : undefined"
|
||||
:aria-disabled="disabled ? 'true' : undefined"
|
||||
:role="disabled ? 'link' : undefined"
|
||||
:rel="rel"
|
||||
:target="target"
|
||||
:class="active !== undefined ? (active ? activeClass : inactiveClass) : { [activeClass]: isActive, [inactiveClass]: !isActive }"
|
||||
:tabindex="!disabled ? undefined : -1"
|
||||
@click="(e) => (!disabled && !isExternal) && navigate(e)"
|
||||
>
|
||||
<slot v-bind="{ isActive: active !== undefined ? active : isActive }" />
|
||||
</a>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
@ -28,6 +28,18 @@ export default {
|
||||
'lg': 'p-2.5',
|
||||
'xl': 'p-2.5',
|
||||
},
|
||||
icon: {
|
||||
base: 'flex-shrink-0',
|
||||
loading: 'animate-spin',
|
||||
size: {
|
||||
'2xs': 'h-4 w-4',
|
||||
'xs': 'h-4 w-4',
|
||||
'sm': 'h-5 w-5',
|
||||
'md': 'h-5 w-5',
|
||||
'lg': 'h-5 w-5',
|
||||
'xl': 'h-6 w-6',
|
||||
},
|
||||
},
|
||||
color: {},
|
||||
variant: {
|
||||
solid:
|
||||
@ -43,5 +55,6 @@ export default {
|
||||
size: 'sm',
|
||||
color: 'primary',
|
||||
variant: 'solid',
|
||||
loadingIcon: 'loading',
|
||||
},
|
||||
}
|
||||
|
83
src/runtime/utils/link.ts
Normal file
83
src/runtime/utils/link.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import type { PropType } from 'vue'
|
||||
import type { RouteLocationRaw } from '#vue-router'
|
||||
import type { NuxtLinkProps } from '#app'
|
||||
|
||||
// NuxtLink props
|
||||
// ref: https://github.com/nuxt/ui/blob/51c8b8e3e59d7eceff72625650a199fcf7c6feca/src/runtime/utils/link.ts#L5-L81
|
||||
export const nuxtLinkProps = {
|
||||
to: {
|
||||
type: [String, Object] as PropType<RouteLocationRaw>,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
href: {
|
||||
type: [String, Object] as PropType<RouteLocationRaw>,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
|
||||
// Attributes
|
||||
target: {
|
||||
type: String as PropType<NuxtLinkProps['target']>,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
rel: {
|
||||
type: String as PropType<any>,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
noRel: {
|
||||
type: Boolean as PropType<NuxtLinkProps['noRel']>,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
|
||||
// Prefetching
|
||||
prefetch: {
|
||||
type: Boolean as PropType<NuxtLinkProps['prefetch']>,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
noPrefetch: {
|
||||
type: Boolean as PropType<NuxtLinkProps['noPrefetch']>,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
|
||||
// Styling
|
||||
activeClass: {
|
||||
type: String as PropType<NuxtLinkProps['activeClass']>,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
exactActiveClass: {
|
||||
type: String as PropType<NuxtLinkProps['exactActiveClass']>,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
prefetchedClass: {
|
||||
type: String as PropType<NuxtLinkProps['prefetchedClass']>,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
|
||||
// Vue Router's `<RouterLink>` additional props
|
||||
replace: {
|
||||
type: Boolean as PropType<NuxtLinkProps['replace']>,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
ariaCurrentValue: {
|
||||
type: String as PropType<NuxtLinkProps['ariaCurrentValue']>,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
|
||||
// Edge cases handling
|
||||
external: {
|
||||
type: Boolean as PropType<NuxtLinkProps['external']>,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
} as const
|
@ -36,3 +36,18 @@ export const getValueByPath = (
|
||||
|
||||
return result !== undefined ? result : defaultValue
|
||||
}
|
||||
|
||||
export const getNonUndefinedValuesFromObject = (
|
||||
obj: object,
|
||||
srcObj: object,
|
||||
): object => {
|
||||
const keys = Object.keys(srcObj) as []
|
||||
|
||||
return keys.reduce((acc, key) => {
|
||||
if (obj[key] !== undefined) {
|
||||
acc[key] = obj[key]
|
||||
}
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user