Merge pull request #7 from Chank1e/chank1e/button

feat: button, button-group, spinner
This commit is contained in:
Zoltán Szőgyényi
2022-07-04 10:03:16 +03:00
committed by GitHub
26 changed files with 1813 additions and 134 deletions

View File

@@ -17,6 +17,8 @@ function buildSidebar() {
function getComponents() {
return [
{ text: 'Button', link: '/guide/button/button.md' },
{ text: 'Button Group', link: '/guide/buttonGroup/buttonGroup.md' },
{ text: 'Spinner', link: '/guide/spinner/spinner.md' },
]
}

View File

@@ -9,6 +9,10 @@ import ButtonPrefixExample from './examples/ButtonPrefixExample.vue';
import ButtonSuffixExample from './examples/ButtonSuffixExample.vue';
import ButtonOutlineGradientExample from './examples/ButtonOutlineGradientExample.vue';
import ButtonGradientShadowExample from './examples/ButtonGradientShadowExample.vue';
import ButtonIconExample from './examples/ButtonIconExample.vue';
import ButtonSquareExample from './examples/ButtonSquareExample.vue';
import ButtonDisabledExample from './examples/ButtonDisabledExample.vue';
import ButtonLoadingExample from './examples/ButtonLoadingExample.vue';
</script>
# Button
@@ -210,6 +214,17 @@ import { Button } from 'flowbite-vue'
## Prop - shadow
```typescript
type ButtonMonochromeGradient = 'blue' | 'green' | 'cyan' | 'teal' | 'lime' | 'red' | 'pink' | 'purple'
defineProps({
shadow: {
type: [String, null] as PropType<ButtonMonochromeGradient | '' | null>,
default: null,
},
})
```
<ButtonGradientShadowExample />
```vue
@@ -229,6 +244,153 @@ import { Button } from 'flowbite-vue'
```
## Prop - square
```typescript
defineProps({
square: {
type: Boolean,
default: false,
},
})
```
<ButtonSquareExample />
```vue
<script setup>
import { Button } from 'flowbite-vue'
</script>
<template>
<Button gradient="red-yellow" square>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</Button>
<Button color="default" pill square>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</Button>
<Button color="dark" outline square>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</Button>
<Button color="yellow" pill outline square>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</Button>
</template>
```
## Prop - loading
```typescript
defineProps({
loading: {
type: Boolean,
default: false,
},
loadingPosition: {
type: String as PropType<'suffix' | 'prefix'>,
default: 'prefix',
},
})
```
<ButtonLoadingExample />
```vue
<script setup>
import { Button } from 'flowbite-vue'
const loading = ref(false)
</script>
<template>
<Button gradient="purple-blue" outline :disabled="loading" :loading="loading" @click="loading = !loading" size="xs">
Click me
</Button>
<Button gradient="red-yellow" :loading="loading" @click="loading = !loading" size="sm">
Click me
</Button>
<Button outline color="default" loading-position="suffix" :loading="loading" @click="loading = !loading">
Click me
<template #suffix>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z"
clip-rule="evenodd"></path>
</svg>
</template>
</Button>
<Button gradient="green-blue" :loading="loading" @click="loading = !loading" size="lg">
Click me
</Button>
<Button gradient="pink" :loading="loading" @click="loading = !loading" size="xl">
Click me
</Button>
</template>
```
## Prop - disabled
```typescript
defineProps({
disabled: {
type: Boolean,
default: false,
},
})
```
<ButtonDisabledExample />
```vue
<script setup>
import { Button } from 'flowbite-vue'
</script>
<template>
<Button gradient="red-yellow" square>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</Button>
<Button color="default" pill square>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</Button>
<Button color="dark" outline square>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</Button>
<Button color="yellow" pill outline square>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</Button>
</template>
```
## Slot - default
<ButtonIconExample />
```vue
<script setup>
import { Button } from 'flowbite-vue'
</script>
<template>
<Button gradient="purple-blue" square>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</Button>
<Button color="default" pill square>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</Button>
<Button gradient="green-blue" square>
Close something
</Button>
<Button color="default" pill outline square>
Open something
<template #suffix>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</template>
</Button>
</template>
```
## Slot - prefix
<ButtonPrefixExample />
@@ -263,6 +425,4 @@ import { Button } from 'flowbite-vue'
</template>
</Button>
</template>
```
```

View File

@@ -0,0 +1,12 @@
<template>
<div class="inline-flex align-center gap-2 flex-wrap">
<Button color="default" disabled>Default</Button>
<Button color="default" outline disabled>Default outline</Button>
<Button gradient="red" disabled>Red gradient</Button>
<Button gradient="red-yellow" disabled>Red to yellow gradient</Button>
<Button gradient="red-yellow" outline disabled>Red to yellow outline</Button>
</div>
</template>
<script setup>
import { Button } from '../../../../src/index'
</script>

View File

@@ -0,0 +1,22 @@
<template>
<div class="inline-flex align-center gap-2 flex-wrap">
<Button gradient="purple-blue" square>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</Button>
<Button color="default" pill square>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</Button>
<Button gradient="green-blue" square>
Close something
</Button>
<Button color="default" pill outline square>
Open something
<template #suffix>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</template>
</Button>
</div>
</template>
<script setup>
import { Button } from '../../../../src/index'
</script>

View File

@@ -0,0 +1,32 @@
<template>
<div class="inline-flex items-center gap-2 flex-wrap">
<Button gradient="purple-blue" outline :disabled="loading" :loading="loading" @click="loading = !loading" size="xs">
Click me
</Button>
<Button gradient="red-yellow" :loading="loading" @click="loading = !loading" size="sm">
Click me
</Button>
<Button outline color="default" loading-position="suffix" :loading="loading" @click="loading = !loading">
Click me
<template #suffix>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z"
clip-rule="evenodd"></path>
</svg>
</template>
</Button>
<Button gradient="green-blue" :loading="loading" @click="loading = !loading" size="lg">
Click me
</Button>
<Button gradient="pink" :loading="loading" @click="loading = !loading" size="xl">
Click me
</Button>
</div>
</template>
<script lang="ts" setup>
import {ref} from 'vue'
import {Button} from '../../../../src/index'
const loading = ref(false)
</script>

View File

@@ -2,7 +2,7 @@
<div class="inline-flex align-center gap-2 flex-wrap">
<Button color="default">
<template #prefix>
<svg class="w-5 h-5 mr-2 -ml-1" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M3 1a1 1 0 000 2h1.22l.305 1.222a.997.997 0 00.01.042l1.358 5.43-.893.892C3.74 11.846 4.632 14 6.414 14H15a1 1 0 000-2H6.414l1-1H14a1 1 0 00.894-.553l3-6A1 1 0 0017 3H6.28l-.31-1.243A1 1 0 005 1H3zM16 16.5a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zM6.5 18a1.5 1.5 0 100-3 1.5 1.5 0 000 3z"></path></svg>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M3 1a1 1 0 000 2h1.22l.305 1.222a.997.997 0 00.01.042l1.358 5.43-.893.892C3.74 11.846 4.632 14 6.414 14H15a1 1 0 000-2H6.414l1-1H14a1 1 0 00.894-.553l3-6A1 1 0 0017 3H6.28l-.31-1.243A1 1 0 005 1H3zM16 16.5a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zM6.5 18a1.5 1.5 0 100-3 1.5 1.5 0 000 3z"></path></svg>
</template>
Buy
</Button>

View File

@@ -0,0 +1,19 @@
<template>
<div class="inline-flex align-center gap-2 flex-wrap">
<Button gradient="red-yellow" square>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</Button>
<Button color="default" pill square>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</Button>
<Button color="dark" outline square>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</Button>
<Button color="yellow" pill outline square>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</Button>
</div>
</template>
<script setup>
import { Button } from '../../../../src/index'
</script>

View File

@@ -3,7 +3,7 @@
<Button color="default">
Choose plan
<template #suffix>
<svg class="w-5 h-5 ml-2 -mr-1" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</template>
</Button>
</div>

View File

@@ -0,0 +1,49 @@
<script setup>
import ButtonGroupBasicExample from './examples/ButtonGroupBasicExample.vue';
import ButtonGroupIconExample from './examples/ButtonGroupIconExample.vue';
</script>
# Button Group
reference: [https://flowbite.com/docs/components/button-group/](https://flowbite.com/docs/components/button-group/)
## Basic example
<ButtonGroupBasicExample />
```vue
<script setup>
import { ButtonGroup, Button } from 'flowbite-vue'
</script>
<template>
<button-group>
<Button>hello world</Button>
<Button color="purple">hello world</Button>
<Button color="alternative">hello world</Button>
<Button color="red">hello world</Button>
</button-group>
</template>
```
## Buttons with icons
<ButtonGroupIconExample />
```vue
<script setup>
import { ButtonGroup, Button } from 'flowbite-vue'
</script>
<template>
<button-group>
<Button outline>Button1</Button>
<Button outline>Button2</Button>
<Button outline>Button3</Button>
<Button outline>
hello world
<template #suffix>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</template>
</Button>
</button-group>
</template>
```

View File

@@ -0,0 +1,11 @@
<template>
<button-group>
<Button>hello world</Button>
<Button color="purple">hello world</Button>
<Button color="alternative">hello world</Button>
<Button color="red">hello world</Button>
</button-group>
</template>
<script setup>
import { ButtonGroup, Button } from '../../../../src/index'
</script>

View File

@@ -0,0 +1,16 @@
<template>
<button-group>
<Button outline>Button1</Button>
<Button outline>Button2</Button>
<Button outline>Button3</Button>
<Button outline>
hello world
<template #suffix>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</template>
</Button>
</button-group>
</template>
<script setup>
import { ButtonGroup, Button } from '../../../../src/index'
</script>

View File

@@ -0,0 +1,8 @@
<template>
<div class="inline-flex align-center gap-2 flex-wrap">
<spinner />
</div>
</template>
<script setup>
import { Spinner } from '../../../../src/index'
</script>

View File

@@ -0,0 +1,15 @@
<template>
<div class="inline-flex align-center gap-2 flex-wrap">
<spinner color="blue" size="6" />
<spinner color="pink" size="8" />
<spinner color="gray" size="10" />
<spinner color="green" size="12" />
<spinner color="purple" size="10" />
<spinner color="white" size="8" />
<spinner color="yellow" size="6" />
<spinner color="red" size="4" />
</div>
</template>
<script setup>
import { Spinner } from '../../../../src/index'
</script>

View File

@@ -0,0 +1,12 @@
<template>
<div class="inline-flex align-center gap-2 flex-wrap">
<spinner />
<spinner size="6" />
<spinner size="8" />
<spinner size="10" />
<spinner size="12" />
</div>
</template>
<script setup>
import { Spinner } from '../../../../src/index'
</script>

View File

@@ -0,0 +1,85 @@
<script setup>
import SpinnerBasicExample from './examples/SpinnerBasicExample.vue';
import SpinnerSizeExample from './examples/SpinnerSizeExample.vue';
import SpinnerColorExample from './examples/SpinnerColorExample.vue';
</script>
# Spinner
reference: [https://flowbite.com/docs/components/spinner/](https://flowbite.com/docs/components/spinner/)
## Basic example
<SpinnerBasicExample />
```vue
<script setup>
import { Spinner } from 'flowbite-vue'
</script>
<template>
<spinner />
</template>
```
## Prop - size
```typescript
type SpinnerSize = '0' | 'px' | '0.5' | '1' | '1.5' | '2' | '2.5' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '10' | '11' | '12'
defineProps({
size: {
type: String as PropType<SpinnerSize>, // any string for w-${size} and h-${size} tailwind classes
default: '4',
},
})
```
<SpinnerSizeExample />
```vue
<script setup>
import { Spinner } from 'flowbite-vue'
</script>
<template>
<spinner />
<spinner size="6" />
<spinner size="8" />
<spinner size="10" />
<spinner size="12" />
</template>
```
## Prop - color
```typescript
type SpinnerColor = 'blue' | 'gray' | 'green' | 'red' | 'yellow' | 'pink' | 'purple' | 'white'
defineProps({
color: {
type: String as PropType<SpinnerColor>,
default: 'blue',
},
})
```
<SpinnerColorExample />
```vue
<script setup>
import { Spinner } from 'flowbite-vue'
</script>
<template>
<spinner color="blue" size="6" />
<spinner color="pink" size="8" />
<spinner color="gray" size="10" />
<spinner color="green" size="12" />
<spinner color="purple" size="10" />
<spinner color="white" size="8" />
<spinner color="yellow" size="6" />
<spinner color="red" size="4" />
</template>
```

843
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,9 @@
"build": "vite build",
"build-types": "vue-tsc --declaration --emitDeclarationOnly",
"lint": "eslint --ext .ts,.vue --ignore-path .gitignore --fix src",
"format": "prettier . --write"
"format": "prettier . --write",
"test": "vitest",
"coverage": "vitest run --coverage"
},
"peerDependencies": {
"vue": "^3.2.37"
@@ -33,18 +35,22 @@
"@typescript-eslint/parser": "^5.30.0",
"@vitejs/plugin-vue": "^2.3.3",
"@vue/compiler-sfc": "^3.2.37",
"@vue/test-utils": "^2.0.0",
"@vue/tsconfig": "^0.1.3",
"c8": "^7.11.3",
"class-names": "^1.0.0",
"eslint": "^8.18.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-vue": "^9.1.1",
"flowbite": "^1.4.2",
"jsdom": "^20.0.0",
"postcss": "^8.4.6",
"prettier": "^2.3.2",
"tailwindcss": "^3.1.4",
"typescript": "^4.7.3",
"vite": "^2.4.3",
"vitepress": "^1.0.0-alpha.4",
"vitest": "^0.16.0",
"vue-eslint-parser": "^9.0.3",
"vue-tsc": "^0.38.2"
},

View File

@@ -1,17 +1,38 @@
<template>
<button type="button" :class="bindClasses">
<slot name="prefix" />
<button type="button" :class="wrapperClasses" :disabled="disabled">
<div v-if="!isOutlineGradient && ($slots.prefix || loadingPrefix)" class="mr-2"> <!--automatically add mr class if slot provided or loading -->
<spinner :color="spinnerColor" :size="spinnerSize" v-if="loadingPrefix" />
<slot name="prefix" v-else />
</div>
<span :class="spanClasses">
<div v-if="isOutlineGradient && ($slots.prefix || loadingPrefix)" class="mr-2"> <!--if outline gradient - need to place slots inside span -->
<spinner :color="spinnerColor" :size="spinnerSize" v-if="loadingPrefix" />
<slot name="prefix" v-else />
</div>
<slot/>
<div v-if="isOutlineGradient && ($slots.suffix || loadingSuffix)" class="ml-2"> <!--if outline gradient - need to place slots inside span -->
<spinner :color="spinnerColor" :size="spinnerSize" v-if="loadingSuffix" />
<slot name="suffix" v-else />
</div>
</span>
<slot name="suffix" />
<div v-if="!isOutlineGradient && ($slots.suffix || loadingSuffix)" class="ml-2"> <!--automatically add ml class if slot provided or loading -->
<spinner :color="spinnerColor" :size="spinnerSize" v-if="loadingSuffix" />
<slot name="suffix" v-else />
</div>
</button>
</template>
<script lang="ts" setup>
import {computed, useSlots} from 'vue'
import { computed } from 'vue'
import type { PropType } from 'vue'
import classNames from 'classnames'
import Spinner from '../Spinner/Spinner.vue'
import { useButtonClasses } from './useButtonClasses'
import { useButtonSpinner } from './useButtonSpinner'
export type ButtonMonochromeGradient = 'blue' | 'green' | 'cyan' | 'teal' | 'lime' | 'red' | 'pink' | 'purple'
export type ButtonDuotoneGradient = 'purple-blue' | 'cyan-blue' | 'green-blue' | 'purple-pink' | 'pink-orange' | 'teal-lime' | 'red-yellow'
@@ -33,140 +54,42 @@ const props = defineProps({
type: String as PropType<ButtonSize>,
default: 'md',
},
shadow: {
type: [String, null] as PropType<ButtonMonochromeGradient | '' | null>,
default: null,
},
pill: {
type: Boolean,
default: false,
},
square: {
type: Boolean,
default: false,
},
outline: {
type: Boolean,
default: false,
},
shadow: {
type: [String, null] as PropType<ButtonMonochromeGradient | '' | null>,
default: null,
loading: {
type: Boolean,
default: false,
},
loadingPosition: {
type: String as PropType<'suffix' | 'prefix'>,
default: 'prefix',
},
disabled: {
type: Boolean,
default: false,
},
})
const slots = useSlots()
const isOutlineGradient = computed(() => props.outline && props.gradient)
const bindClasses = computed(() => {
const isGradient = !!props.gradient
const isColor = !!props.color
const isOutline = props.outline
const loadingPrefix = computed(() => props.loading && props.loadingPosition === 'prefix')
const loadingSuffix = computed(() => props.loading && props.loadingPosition === 'suffix')
let backgroundClass = ''
const { wrapperClasses, spanClasses } = useButtonClasses(props)
const { color: spinnerColor, size: spinnerSize } = useButtonSpinner(props)
if(isGradient && isOutline) {
if(['blue', 'green', 'cyan', 'teal', 'lime', 'red', 'pink', 'purple'].includes(props.gradient)) { // invalid gradients for outline
backgroundClass = buttonGradientClasses[props.gradient]
} else {
backgroundClass = buttonOutlineGradientClasses[props.gradient as unknown as keyof typeof buttonOutlineGradientClasses]
}
} else if(isGradient) {
backgroundClass = buttonGradientClasses[props.gradient]
} else if(isColor && isOutline) {
if(['alternative', 'light'].includes(props.color)) { // invalid colors for outline
backgroundClass = buttonColorClasses[props.color]
} else {
backgroundClass = buttonOutlineColorClasses[props.color as unknown as keyof typeof buttonOutlineColorClasses]
}
} else {
backgroundClass = buttonColorClasses[props.color]
}
let shadowClass = ''
if(props.shadow === '') {
if(props.gradient && ['blue', 'green', 'cyan', 'teal', 'lime', 'red', 'pink', 'purple'].includes(props.gradient)) {
shadowClass = buttonShadowClasses[props.gradient as unknown as keyof typeof buttonShadowClasses]
}
} else if(typeof props.shadow === 'string') {
if(['blue', 'green', 'cyan', 'teal', 'lime', 'red', 'pink', 'purple'].includes(props.shadow)) {
shadowClass = buttonShadowClasses[props.shadow as unknown as keyof typeof buttonShadowClasses]
}
}
return classNames(
backgroundClass,
shadowClass,
(isGradient && isOutline) ? 'p-0.5' : buttonSizeClasses[props.size],
props.pill ? '!rounded-full' : '',
(slots.prefix || slots.suffix) ? 'inline-flex items-center' : '',
)
})
const spanClasses = computed(() => {
if(!!props.gradient && props.outline)
return classNames(
'relative transition-all ease-in duration-75 bg-white dark:bg-gray-900 rounded-md group-hover:bg-opacity-0',
buttonSizeClasses[props.size],
)
return ''
})
const buttonColorClasses: Record<ButtonVariant, string> = {
default: 'text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800',
alternative: 'font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700',
dark: 'text-white bg-gray-800 hover:bg-gray-900 focus:outline-none focus:ring-4 focus:ring-gray-300 font-medium rounded-lg dark:bg-gray-800 dark:hover:bg-gray-700 dark:focus:ring-gray-700 dark:border-gray-700',
light: 'text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 font-medium rounded-lg dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700',
green: 'focus:outline-none text-white bg-green-700 hover:bg-green-800 focus:ring-4 focus:ring-green-300 font-medium rounded-lg dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-800',
red: 'focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-900',
yellow: 'focus:outline-none text-white bg-yellow-400 hover:bg-yellow-500 focus:ring-4 focus:ring-yellow-300 font-medium rounded-lg dark:focus:ring-yellow-900',
purple: 'focus:outline-none text-white bg-purple-700 hover:bg-purple-800 focus:ring-4 focus:ring-purple-300 font-medium rounded-lg dark:bg-purple-600 dark:hover:bg-purple-700 dark:focus:ring-purple-900',
}
const buttonOutlineColorClasses: Record<Exclude<ButtonVariant, 'light' | 'alternative'>, string> = {
dark: 'text-gray-900 hover:text-white border border-gray-800 hover:bg-gray-900 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg text-sm text-center dark:border-gray-600 dark:text-gray-400 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-800',
default: 'text-blue-700 hover:text-white border border-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm text-center dark:border-blue-500 dark:text-blue-500 dark:hover:text-white dark:hover:bg-blue-600 dark:focus:ring-blue-800',
green: 'text-green-700 hover:text-white border border-green-700 hover:bg-green-800 focus:ring-4 focus:outline-none focus:ring-green-300 font-medium rounded-lg text-sm text-center dark:border-green-500 dark:text-green-500 dark:hover:text-white dark:hover:bg-green-600 dark:focus:ring-green-800',
purple: 'text-purple-700 hover:text-white border border-purple-700 hover:bg-purple-800 focus:ring-4 focus:outline-none focus:ring-purple-300 font-medium rounded-lg text-sm text-center dark:border-purple-400 dark:text-purple-400 dark:hover:text-white dark:hover:bg-purple-500 dark:focus:ring-purple-900',
red: 'text-red-700 hover:text-white border border-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm text-center dark:border-red-500 dark:text-red-500 dark:hover:text-white dark:hover:bg-red-600 dark:focus:ring-red-900',
yellow: 'text-yellow-400 hover:text-white border border-yellow-400 hover:bg-yellow-500 focus:ring-4 focus:outline-none focus:ring-yellow-300 font-medium rounded-lg text-sm text-center dark:border-yellow-300 dark:text-yellow-300 dark:hover:text-white dark:hover:bg-yellow-400 dark:focus:ring-yellow-900',
}
const buttonGradientClasses: Record<ButtonGradient, string> = {
'cyan-blue': 'text-white bg-gradient-to-r from-cyan-500 to-blue-500 hover:bg-gradient-to-bl focus:ring-4 focus:outline-none focus:ring-cyan-300 dark:focus:ring-cyan-800 font-medium rounded-lg',
'green-blue': 'text-white bg-gradient-to-br from-green-400 to-blue-600 hover:bg-gradient-to-bl focus:ring-4 focus:outline-none focus:ring-green-200 dark:focus:ring-green-800 font-medium rounded-lg',
'pink-orange': 'text-white bg-gradient-to-br from-pink-500 to-orange-400 hover:bg-gradient-to-bl focus:ring-4 focus:outline-none focus:ring-pink-200 dark:focus:ring-pink-800 font-medium rounded-lg',
'purple-blue': 'text-white bg-gradient-to-br from-purple-600 to-blue-500 hover:bg-gradient-to-bl focus:ring-4 focus:outline-none focus:ring-blue-300 dark:focus:ring-blue-800 font-medium rounded-lg',
'purple-pink': 'text-white bg-gradient-to-r from-purple-500 to-pink-500 hover:bg-gradient-to-l focus:ring-4 focus:outline-none focus:ring-purple-200 dark:focus:ring-purple-800 font-medium rounded-lg',
'red-yellow': 'text-gray-900 bg-gradient-to-r from-red-200 via-red-300 to-yellow-200 hover:bg-gradient-to-bl focus:ring-4 focus:outline-none focus:ring-red-100 dark:focus:ring-red-400 font-medium rounded-lg',
'teal-lime': 'text-gray-900 bg-gradient-to-r from-teal-200 to-lime-200 hover:bg-gradient-to-l hover:from-teal-200 hover:to-lime-200 focus:ring-4 focus:outline-none focus:ring-lime-200 dark:focus:ring-teal-700 font-medium rounded-lg',
'blue': 'text-white bg-gradient-to-r from-blue-500 via-blue-600 to-blue-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-blue-300 dark:focus:ring-blue-800 rounded-lg',
'cyan': 'text-white bg-gradient-to-r from-cyan-500 via-cyan-600 to-cyan-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-cyan-300 dark:focus:ring-cyan-800 rounded-lg',
'green': 'text-white bg-gradient-to-r from-green-500 via-green-600 to-green-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-green-300 dark:focus:ring-green-800 rounded-lg',
'lime': 'text-gray-900 bg-gradient-to-r from-lime-500 via-lime-600 to-lime-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-lime-300 dark:focus:ring-lime-800 rounded-lg',
'pink': 'text-white bg-gradient-to-r from-pink-500 via-pink-600 to-pink-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-pink-300 dark:focus:ring-pink-800 rounded-lg',
'purple': 'text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800 rounded-lg',
'red': 'text-white bg-gradient-to-r from-red-500 via-red-600 to-red-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 rounded-lg',
'teal': 'text-white bg-gradient-to-r from-teal-500 via-teal-600 to-teal-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-teal-300 dark:focus:ring-teal-800 rounded-lg',
}
const buttonOutlineGradientClasses: Record<ButtonDuotoneGradient, string> = {
'cyan-blue': 'relative inline-flex items-center justify-center overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-cyan-500 to-blue-500 group-hover:from-cyan-500 group-hover:to-blue-500 hover:text-white dark:text-white focus:ring-4 focus:outline-none focus:ring-cyan-200 dark:focus:ring-cyan-800',
'green-blue': 'relative inline-flex items-center justify-center overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-green-400 to-blue-600 group-hover:from-green-400 group-hover:to-blue-600 hover:text-white dark:text-white focus:ring-4 focus:outline-none focus:ring-green-200 dark:focus:ring-green-800',
'pink-orange': 'relative inline-flex items-center justify-center overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-pink-500 to-orange-400 group-hover:from-pink-500 group-hover:to-orange-400 hover:text-white dark:text-white focus:ring-4 focus:outline-none focus:ring-pink-200 dark:focus:ring-pink-800',
'purple-blue': 'relative inline-flex items-center justify-center overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-purple-600 to-blue-500 group-hover:from-purple-600 group-hover:to-blue-500 hover:text-white dark:text-white focus:ring-4 focus:outline-none focus:ring-blue-300 dark:focus:ring-blue-800',
'purple-pink': 'relative inline-flex items-center justify-center overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-purple-500 to-pink-500 group-hover:from-purple-500 group-hover:to-pink-500 hover:text-white dark:text-white focus:ring-4 focus:outline-none focus:ring-purple-200 dark:focus:ring-purple-800',
'red-yellow': 'relative inline-flex items-center justify-center overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-red-200 via-red-300 to-yellow-200 group-hover:from-red-200 group-hover:via-red-300 group-hover:to-yellow-200 dark:text-white dark:hover:text-gray-900 focus:ring-4 focus:outline-none focus:ring-red-100 dark:focus:ring-red-400',
'teal-lime': 'relative inline-flex items-center justify-center overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-teal-300 to-lime-300 group-hover:from-teal-300 group-hover:to-lime-300 dark:text-white dark:hover:text-gray-900 focus:ring-4 focus:outline-none focus:ring-lime-200 dark:focus:ring-lime-800',
}
const buttonSizeClasses: Record<ButtonSize, string> = {
xs: 'text-xs px-2 py-1',
sm: 'text-sm px-3 py-1.5',
md: 'text-sm px-4 py-2',
lg: 'text-base px-5 py-2.5',
xl: 'text-base px-6 py-3',
}
const buttonShadowClasses: Record<ButtonMonochromeGradient, string> = {
'blue': 'shadow-lg shadow-blue-500/50 dark:shadow-lg dark:shadow-blue-800/80',
'cyan': 'shadow-lg shadow-cyan-500/50 dark:shadow-lg dark:shadow-cyan-800/80',
'green': 'shadow-lg shadow-green-500/50 dark:shadow-lg dark:shadow-green-800/80',
'lime': 'shadow-lg shadow-lime-500/50 dark:shadow-lg dark:shadow-lime-800/80',
'pink': 'shadow-lg shadow-pink-500/50 dark:shadow-lg dark:shadow-pink-800/80',
'purple': 'shadow-lg shadow-purple-500/50 dark:shadow-lg dark:shadow-purple-800/80',
'red': 'shadow-lg shadow-red-500/50 dark:shadow-lg dark:shadow-red-800/80',
'teal': 'shadow-lg shadow-teal-500/50 dark:shadow-lg dark:shadow-teal-800/80',
}
</script>

View File

@@ -0,0 +1,45 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from '../Button.vue'
describe('Button', () => {
it('renders correct text', () => {
const wrapper = mount(Button, { props: {}, slots: { default: 'test' } })
expect(wrapper.text()).toBe('test')
})
it('provides correct classes for default color button', () => {
const defaultButtonClasses = [
'text-white',
'bg-blue-700',
'hover:bg-blue-800',
'focus:ring-4',
'focus:ring-blue-300',
'font-medium',
'rounded-lg',
'dark:bg-blue-600',
'dark:hover:bg-blue-700',
'focus:outline-none',
'dark:focus:ring-blue-800',
'text-sm',
'px-4',
'py-2',
]
const wrapper = mount(Button, { props: { color: 'default' } })
const classes = wrapper.classes()
defaultButtonClasses.forEach(cl => expect(classes).toContain(cl))
})
it('provides correct classes for XL size', () => {
const xlButtonSizeClasses = [
'text-base', 'px-6', 'py-3',
]
const wrapper = mount(Button, { props: { size: 'xl' } })
const classes = wrapper.classes()
xlButtonSizeClasses.forEach(cl => expect(classes).toContain(cl))
})
})

View File

@@ -0,0 +1,253 @@
import type { Ref } from 'vue'
import { computed, useSlots } from 'vue'
import classNames from 'classnames'
import type { ButtonDuotoneGradient, ButtonGradient, ButtonMonochromeGradient, ButtonSize, ButtonVariant } from './Button.vue'
const buttonColorClasses: { hover: Record<ButtonVariant, string>, default: Record<ButtonVariant, string> } = {
default: {
default: 'text-white bg-blue-700 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg dark:bg-blue-600 focus:outline-none dark:focus:ring-blue-800',
alternative: 'font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600',
dark: 'text-white bg-gray-800 focus:outline-none focus:ring-4 focus:ring-gray-300 font-medium rounded-lg dark:bg-gray-800 dark:focus:ring-gray-700 dark:border-gray-700',
light: 'text-gray-900 bg-white border border-gray-300 focus:outline-none focus:ring-4 focus:ring-gray-200 font-medium rounded-lg dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:focus:ring-gray-700',
green: 'focus:outline-none text-white bg-green-700 focus:ring-4 focus:ring-green-300 font-medium rounded-lg dark:bg-green-600 dark:focus:ring-green-800',
red: 'focus:outline-none text-white bg-red-700 focus:ring-4 focus:ring-red-300 font-medium rounded-lg dark:bg-red-600 dark:focus:ring-red-900',
yellow: 'focus:outline-none text-white bg-yellow-400 focus:ring-4 focus:ring-yellow-300 font-medium rounded-lg dark:focus:ring-yellow-900',
purple: 'focus:outline-none text-white bg-purple-700 focus:ring-4 focus:ring-purple-300 font-medium rounded-lg dark:bg-purple-600 dark:focus:ring-purple-900',
},
hover: {
default: 'hover:bg-blue-800 dark:hover:bg-blue-700',
alternative: 'hover:bg-gray-100 hover:text-blue-700 dark:hover:text-white dark:hover:bg-gray-700',
dark: 'hover:bg-gray-900 dark:hover:bg-gray-700',
light: 'hover:bg-gray-100 dark:hover:border-gray-600',
green: 'hover:bg-green-800 dark:hover:bg-green-700',
red: 'hover:bg-red-800 dark:hover:bg-red-700',
yellow: 'hover:bg-yellow-500',
purple: 'hover:bg-purple-800 dark:hover:bg-purple-700',
},
}
const buttonOutlineColorClasses: {
hover: Record<Exclude<ButtonVariant, 'light' | 'alternative'>, string>,
default: Record<Exclude<ButtonVariant, 'light' | 'alternative'>, string>
} = {
default: {
dark: 'text-gray-900 border border-gray-800 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg text-sm text-center dark:border-gray-600 dark:text-gray-400 dark:focus:ring-gray-800',
default: 'text-blue-700 border border-blue-700 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm text-center dark:border-blue-500 dark:text-blue-500 dark:focus:ring-blue-800',
green: 'text-green-700 border border-green-700 focus:ring-4 focus:outline-none focus:ring-green-300 font-medium rounded-lg text-sm text-center dark:border-green-500 dark:text-green-500 dark:focus:ring-green-800',
purple: 'text-purple-700 border border-purple-700 focus:ring-4 focus:outline-none focus:ring-purple-300 font-medium rounded-lg text-sm text-center dark:border-purple-400 dark:text-purple-400 dark:focus:ring-purple-900',
red: 'text-red-700 border border-red-700 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm text-center dark:border-red-500 dark:text-red-500 dark:focus:ring-red-900',
yellow: 'text-yellow-400 border border-yellow-400 focus:ring-4 focus:outline-none focus:ring-yellow-300 font-medium rounded-lg text-sm text-center dark:border-yellow-300 dark:text-yellow-300 dark:focus:ring-yellow-900',
},
hover: {
dark: 'hover:text-white hover:bg-gray-900 dark:hover:text-white dark:hover:bg-gray-600',
default: 'hover:text-white hover:bg-blue-800 dark:hover:text-white dark:hover:bg-blue-600',
green: 'hover:text-white hover:bg-green-800 dark:hover:text-white dark:hover:bg-green-600',
purple: 'hover:text-white hover:bg-purple-800 dark:hover:text-white dark:hover:bg-purple-500',
red: 'hover:text-white hover:bg-red-800 dark:hover:text-white dark:hover:bg-red-600',
yellow: 'hover:text-white hover:bg-yellow-500 dark:hover:text-white dark:hover:bg-yellow-400',
},
}
const buttonGradientClasses: { hover: Record<ButtonGradient, string>, default: Record<ButtonGradient, string> } = {
hover: {
'cyan-blue': 'hover:bg-gradient-to-bl',
'green-blue': 'hover:bg-gradient-to-bl',
'pink-orange': 'hover:bg-gradient-to-bl',
'purple-blue': 'hover:bg-gradient-to-bl',
'purple-pink': 'hover:bg-gradient-to-l',
'red-yellow': 'hover:bg-gradient-to-bl',
'teal-lime': 'hover:bg-gradient-to-l hover:from-teal-200 hover:to-lime-200',
blue: 'hover:bg-gradient-to-br',
cyan: 'hover:bg-gradient-to-br',
green: 'hover:bg-gradient-to-br',
lime: 'hover:bg-gradient-to-br',
pink: 'hover:bg-gradient-to-br',
purple: 'hover:bg-gradient-to-br',
red: 'hover:bg-gradient-to-br',
teal: 'hover:bg-gradient-to-br',
},
default: {
'cyan-blue': 'text-white bg-gradient-to-r from-cyan-500 to-blue-500 focus:ring-4 focus:outline-none focus:ring-cyan-300 dark:focus:ring-cyan-800 font-medium rounded-lg',
'green-blue': 'text-white bg-gradient-to-br from-green-400 to-blue-600 focus:ring-4 focus:outline-none focus:ring-green-200 dark:focus:ring-green-800 font-medium rounded-lg',
'pink-orange': 'text-white bg-gradient-to-br from-pink-500 to-orange-400 focus:ring-4 focus:outline-none focus:ring-pink-200 dark:focus:ring-pink-800 font-medium rounded-lg',
'purple-blue': 'text-white bg-gradient-to-br from-purple-600 to-blue-500 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:focus:ring-blue-800 font-medium rounded-lg',
'purple-pink': 'text-white bg-gradient-to-r from-purple-500 to-pink-500 focus:ring-4 focus:outline-none focus:ring-purple-200 dark:focus:ring-purple-800 font-medium rounded-lg',
'red-yellow': 'text-gray-900 bg-gradient-to-r from-red-200 via-red-300 to-yellow-200 focus:ring-4 focus:outline-none focus:ring-red-100 dark:focus:ring-red-400 font-medium rounded-lg',
'teal-lime': 'text-gray-900 bg-gradient-to-r from-teal-200 to-lime-200 focus:ring-4 focus:outline-none focus:ring-lime-200 dark:focus:ring-teal-700 font-medium rounded-lg',
blue: 'text-white bg-gradient-to-r from-blue-500 via-blue-600 to-blue-700 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:focus:ring-blue-800 rounded-lg',
cyan: 'text-white bg-gradient-to-r from-cyan-500 via-cyan-600 to-cyan-700 focus:ring-4 focus:outline-none focus:ring-cyan-300 dark:focus:ring-cyan-800 rounded-lg',
green: 'text-white bg-gradient-to-r from-green-500 via-green-600 to-green-700 focus:ring-4 focus:outline-none focus:ring-green-300 dark:focus:ring-green-800 rounded-lg',
lime: 'text-gray-900 bg-gradient-to-r from-lime-500 via-lime-600 to-lime-700 focus:ring-4 focus:outline-none focus:ring-lime-300 dark:focus:ring-lime-800 rounded-lg',
pink: 'text-white bg-gradient-to-r from-pink-500 via-pink-600 to-pink-700 focus:ring-4 focus:outline-none focus:ring-pink-300 dark:focus:ring-pink-800 rounded-lg',
purple: 'text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800 rounded-lg',
red: 'text-white bg-gradient-to-r from-red-500 via-red-600 to-red-700 focus:ring-4 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 rounded-lg',
teal: 'text-white bg-gradient-to-r from-teal-500 via-teal-600 to-teal-700 focus:ring-4 focus:outline-none focus:ring-teal-300 dark:focus:ring-teal-800 rounded-lg',
},
}
const buttonOutlineGradientClasses: { hover: Record<ButtonDuotoneGradient, string>, default: Record<ButtonDuotoneGradient, string> } = {
default: {
'cyan-blue':
'relative inline-flex items-center justify-center overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-cyan-500 to-blue-500 dark:text-white focus:ring-4 focus:outline-none focus:ring-cyan-200 dark:focus:ring-cyan-800',
'green-blue':
'relative inline-flex items-center justify-center overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-green-400 to-blue-600 dark:text-white focus:ring-4 focus:outline-none focus:ring-green-200 dark:focus:ring-green-800',
'pink-orange':
'relative inline-flex items-center justify-center overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-pink-500 to-orange-400 dark:text-white focus:ring-4 focus:outline-none focus:ring-pink-200 dark:focus:ring-pink-800',
'purple-blue':
'relative inline-flex items-center justify-center overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-purple-600 to-blue-500 dark:text-white focus:ring-4 focus:outline-none focus:ring-blue-300 dark:focus:ring-blue-800',
'purple-pink':
'relative inline-flex items-center justify-center overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-purple-500 to-pink-500 dark:text-white focus:ring-4 focus:outline-none focus:ring-purple-200 dark:focus:ring-purple-800',
'red-yellow':
'relative inline-flex items-center justify-center overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-red-200 via-red-300 to-yellow-200 dark:text-white focus:ring-4 focus:outline-none focus:ring-red-100 dark:focus:ring-red-400',
'teal-lime':
'relative inline-flex items-center justify-center overflow-hidden font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-teal-300 to-lime-300 dark:text-white focus:ring-4 focus:outline-none focus:ring-lime-200 dark:focus:ring-lime-800',
},
hover: {
'cyan-blue': 'group-hover:from-cyan-500 group-hover:to-blue-500 hover:text-white',
'green-blue': 'group-hover:from-green-400 group-hover:to-blue-600 hover:text-white',
'pink-orange': 'group-hover:from-pink-500 group-hover:to-orange-400 hover:text-white',
'purple-blue': 'group-hover:from-purple-600 group-hover:to-blue-500 hover:text-white',
'purple-pink': 'group-hover:from-purple-500 group-hover:to-pink-500 hover:text-white',
'red-yellow': 'group-hover:from-red-200 group-hover:via-red-300 group-hover:to-yellow-200 dark:hover:text-gray-900',
'teal-lime': 'group-hover:from-teal-300 group-hover:to-lime-300 dark:hover:text-gray-900',
},
}
const buttonSizeClasses: Record<ButtonSize, string> = {
xs: 'text-xs px-2 py-1',
sm: 'text-sm px-3 py-1.5',
md: 'text-sm px-4 py-2',
lg: 'text-base px-5 py-2.5',
xl: 'text-base px-6 py-3',
}
const buttonSquareSizeClasses: Record<ButtonSize, string> = {
xs: 'text-xs p-1',
sm: 'text-sm p-1.5',
md: 'text-sm p-2',
lg: 'text-base p-2.5',
xl: 'text-base p-3',
}
const buttonShadowClasses: Record<ButtonMonochromeGradient, string> = {
blue: 'shadow-lg shadow-blue-500/50 dark:shadow-lg dark:shadow-blue-800/80',
cyan: 'shadow-lg shadow-cyan-500/50 dark:shadow-lg dark:shadow-cyan-800/80',
green: 'shadow-lg shadow-green-500/50 dark:shadow-lg dark:shadow-green-800/80',
lime: 'shadow-lg shadow-lime-500/50 dark:shadow-lg dark:shadow-lime-800/80',
pink: 'shadow-lg shadow-pink-500/50 dark:shadow-lg dark:shadow-pink-800/80',
purple: 'shadow-lg shadow-purple-500/50 dark:shadow-lg dark:shadow-purple-800/80',
red: 'shadow-lg shadow-red-500/50 dark:shadow-lg dark:shadow-red-800/80',
teal: 'shadow-lg shadow-teal-500/50 dark:shadow-lg dark:shadow-teal-800/80',
}
export type UseButtonClassesProps = {
pill: boolean
disabled: boolean
loading: boolean
outline: boolean
size: ButtonSize
square: boolean
color: ButtonVariant
gradient: ButtonGradient | null
shadow: ButtonMonochromeGradient | '' | null
}
const simpleGradients = ['blue', 'green', 'cyan', 'teal', 'lime', 'red', 'pink', 'purple']
const alternativeColors = ['alternative', 'light']
export function useButtonClasses(props: UseButtonClassesProps): { wrapperClasses: Ref<string>, spanClasses: Ref<string> } {
const slots = useSlots()
const sizeClasses = computed(() => {
if (props.square) return buttonSquareSizeClasses[props.size]
return buttonSizeClasses[props.size]
})
const bindClasses = computed(() => {
const isGradient = !!props.gradient
const isColor = !!props.color
const isOutline = props.outline
let hoverClass = ''
let backgroundClass = ''
if (isGradient && isOutline) { // GRADIENT AND OUTLINE
if (!simpleGradients.includes(props.gradient!)) {
backgroundClass = buttonOutlineGradientClasses.default[props.gradient as unknown as keyof typeof buttonOutlineGradientClasses.default]
if(!props.disabled)
hoverClass = buttonOutlineGradientClasses.hover[props.gradient as unknown as keyof typeof buttonOutlineGradientClasses.hover]
} else {
console.warn(`cannot use outline prop with "${props.gradient}" gradient`) // TODO: prettify
}
} else if (isGradient) { // JUST GRADIENT
backgroundClass = buttonGradientClasses.default[props.gradient!]
if(!props.disabled)
hoverClass = buttonGradientClasses.hover[props.gradient!]
} else if (isColor && isOutline) { // COLOR AND OUTLINE
if (!alternativeColors.includes(props.color)) {
backgroundClass = buttonOutlineColorClasses.default[props.color as unknown as keyof typeof buttonOutlineColorClasses.default]
if(!props.disabled)
hoverClass = buttonOutlineColorClasses.hover[props.color as unknown as keyof typeof buttonOutlineColorClasses.hover]
} else {
console.warn(`cannot use outline prop with "${props.color}" color`) // TODO: prettify
}
} else { // JUST COLOR
backgroundClass = buttonColorClasses.default[props.color]
if(!props.disabled)
hoverClass = buttonColorClasses.hover[props.color]
}
let shadowClass = ''
if (props.shadow === '') {
// if shadow prop passed without value - try to find color for shadow by gradient
if (props.gradient && simpleGradients.includes(props.gradient)) {
shadowClass = buttonShadowClasses[props.gradient as unknown as keyof typeof buttonShadowClasses]
}
} else if (typeof props.shadow === 'string') {
// if provided color for shadow - use it
if (simpleGradients.includes(props.shadow)) {
shadowClass = buttonShadowClasses[props.shadow as unknown as keyof typeof buttonShadowClasses]
}
}
return classNames(
backgroundClass,
hoverClass,
shadowClass,
props.pill ? '!rounded-full' : '',
props.disabled ? 'cursor-not-allowed opacity-50' : '',
(isGradient && isOutline) ? 'p-0.5' : sizeClasses.value,
(slots.prefix || slots.suffix || props.loading) ? 'inline-flex items-center' : '',
)
})
const spanClasses = computed(() => {
if (!!props.gradient && props.outline) { // ONLY FOR GRADIENT OUTLINE BUTTON
return classNames(
'relative bg-white dark:bg-gray-900 rounded-md inline-flex items-center',
sizeClasses.value,
!props.disabled ? 'group-hover:bg-opacity-0 transition-all ease-in duration-75' : '',
)
}
return ''
})
return {
wrapperClasses: bindClasses,
spanClasses,
}
}

View File

@@ -0,0 +1,44 @@
import type {ButtonGradient, ButtonSize, ButtonVariant} from './Button.vue'
import type {SpinnerColor, SpinnerSize} from '../Spinner/Spinner.vue'
import type {Ref} from 'vue'
import {computed} from 'vue'
export type UseButtonSpinnerProps = {
outline: boolean
size: ButtonSize
color: ButtonVariant
gradient: ButtonGradient | null
}
export function useButtonSpinner(props: UseButtonSpinnerProps): { size: Ref<SpinnerSize>, color: Ref<SpinnerColor> } {
const btnSizeSpinnerSizeMap: Record<ButtonSize, SpinnerSize> = {
lg: '5', md: '4', sm: '3', xl: '6', xs: '2.5',
}
const size = computed<SpinnerSize>(() => {
return btnSizeSpinnerSizeMap[props.size]
})
const color = computed<SpinnerColor>(() => {
if(!props.outline) return 'white'
if(props.gradient) {
if(props.gradient.includes('purple')) return 'purple'
else if(props.gradient.includes('blue')) return 'blue'
else if(props.gradient.includes('pink')) return 'pink'
else if(props.gradient.includes('red')) return 'red'
return 'white'
}
if(['alternative', 'dark', 'light'].includes(props.color)) {
return 'white'
} else if(props.color === 'default') {
return 'blue'
}
return props.color as SpinnerColor
})
return {
size,
color,
}
}

View File

@@ -0,0 +1,21 @@
<template>
<div class="btn-group inline-flex rounded-md shadow-sm" role="group">
<slot />
</div>
</template>
<!-- TODO: maybe use provide/inject to control styles -->
<style>
@tailwind components;
@layer components {
.btn-group > button {
@apply rounded-none
}
.btn-group > button:first-child {
@apply rounded-l-lg
}
.btn-group > button:last-child {
@apply rounded-r-lg
}
}
</style>

View File

@@ -0,0 +1,26 @@
<template>
<svg role="status" :class="spinnerClasses" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
</svg>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import { useSpinnerClasses } from './useSpinnerClasses'
export type SpinnerSize = '0' | 'px' | '0.5' | '1' | '1.5' | '2' | '2.5' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '10' | '11' | '12'
export type SpinnerColor = 'blue' | 'gray' | 'green' | 'red' | 'yellow' | 'pink' | 'purple' | 'white'
const props = defineProps({
size: {
type: String as PropType<SpinnerSize>, // any string for w-${size} and h-${size} tailwind classes, TODO: add all classes
default: '4',
},
color: {
type: String as PropType<SpinnerColor>,
default: 'blue',
},
})
const { spinnerClasses } = useSpinnerClasses(props)
</script>

View File

@@ -0,0 +1,61 @@
import type { SpinnerColor, SpinnerSize } from './Spinner.vue'
import { computed } from 'vue'
import type { Ref } from 'vue'
import classNames from 'classnames'
const sizes: Record<SpinnerSize, string> = {
'0': 'w-0 h-0',
'0.5': 'w-0.5 h-0.5',
'1': 'w-1 h-1',
'1.5': 'w-1.5 h-1.5',
'10': 'w-10 h-10',
'11': 'w-11 h-11',
'12': 'w-12 h-12',
'2': 'w-2 h-2',
'2.5': 'w-2.5 h-2.5',
'3': 'w-3 h-3',
'4': 'w-4 h-4',
'5': 'w-5 h-5',
'6': 'w-6 h-6',
'7': 'w-7 h-7',
'8': 'w-8 h-8',
'9': 'w-9 h-9',
px: 'w-px h-px',
}
const colors: Record<SpinnerColor, string> = {
blue: 'fill-blue-600',
gray: 'fill-gray-600 dark:fill-gray-300',
green: 'fill-green-500',
pink: 'fill-pink-600',
purple: 'fill-purple-600',
red: 'fill-red-600',
yellow: 'fill-yellow-400',
white: 'fill-white',
}
export type UseSpinnerClassesProps = {
size: SpinnerSize
color: SpinnerColor
}
export function useSpinnerClasses(props: UseSpinnerClassesProps): { spinnerClasses: Ref<string> } {
const sizeClasses = computed(() => sizes[props.size])
const colorClasses = computed(() => colors[props.color])
const bgColorClasses = computed(() => 'text-gray-200 dark:text-gray-600')
const animateClasses = computed(() => 'animate-spin')
const spinnerClasses = computed(() => {
return classNames(
sizeClasses.value,
bgColorClasses.value,
colorClasses.value,
animateClasses.value,
)
})
return {
spinnerClasses,
}
}

View File

@@ -1 +1,3 @@
export { default as Button } from './components/Button/Button.vue'
export { default as Spinner } from './components/Spinner/Spinner.vue'
export { default as ButtonGroup } from './components/ButtonGroup/ButtonGroup.vue'

12
vitest.config.ts Normal file
View File

@@ -0,0 +1,12 @@
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [
vue(),
],
test: {
globals: true,
environment: 'jsdom',
},
})