vikunja/frontend/src/components/input/Button.vue

277 lines
5.6 KiB
Vue

<template>
<BaseButton
class="button"
:class="[
variantClass,
{
'is-loading': loading,
'has-no-shadow': !shadow || variant === 'tertiary',
'is-danger': danger,
}
]"
:disabled="disabled || loading"
:style="{
'--button-white-space': wrap ? 'break-spaces' : 'nowrap',
}"
>
<template v-if="$slots.icon">
<span
v-if="$slots.default"
class="icon is-small"
>
<slot name="icon" />
</span>
<slot
v-else
name="icon"
/>
</template>
<span>
<slot />
</span>
</BaseButton>
</template>
<script setup lang="ts">
import {computed} from 'vue'
import BaseButton from '@/components/base/BaseButton.vue'
const props = defineProps<ButtonProps>()
const VARIANT_CLASS_MAP = {
primary: 'is-primary',
secondary: 'is-outlined',
tertiary: 'is-text is-inverted underline-none',
} as const
export type ButtonTypes = keyof typeof VARIANT_CLASS_MAP
export interface ButtonProps {
variant?: ButtonTypes
loading?: boolean
disabled?: boolean
shadow?: boolean
wrap?: boolean
danger?: boolean
}
defineOptions({name: 'XButton'})
const variant = computed(() => (props.variant ?? 'primary') as ButtonTypes)
const shadow = computed(() => (props.shadow ?? true) as boolean)
const wrap = computed(() => (props.wrap ?? true) as boolean)
const variantClass = computed<string>(() => VARIANT_CLASS_MAP[variant.value])
</script>
<style lang="scss" scoped>
.button {
// Button text must stay white regardless of theme, so we use a fixed value
// instead of var(--white) which changes in dark mode.
--button-text-color: #ffffff;
// Base structure (replaces Bulma's .button)
display: inline-flex;
align-items: center;
justify-content: center;
vertical-align: top;
cursor: pointer;
text-align: center;
white-space: var(--button-white-space);
// Custom styles
transition: all $transition;
border: 0;
text-transform: uppercase;
font-size: 0.85rem;
font-weight: bold;
block-size: auto;
min-block-size: $button-height;
box-shadow: var(--shadow-sm);
line-height: 1;
padding-inline: .5rem;
gap: .25rem;
// Default/Primary variant colors
background-color: var(--primary);
color: var(--button-text-color);
border-radius: $radius;
[dir="rtl"] & {
flex-direction: row-reverse;
}
&:hover {
box-shadow: var(--shadow-md);
background-color: var(--primary-dark, color-mix(in srgb, var(--primary) 85%, black));
}
&:focus,
&:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
&.is-active,
&.is-focused,
&:active,
&:focus,
&:focus:not(:active) {
box-shadow: var(--shadow-xs) !important;
}
&[disabled] {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
.icon {
margin: 0 !important;
}
// Primary variant (default, explicit)
&.is-primary {
background-color: var(--primary);
color: var(--button-text-color);
&:hover {
background-color: var(--primary-dark, color-mix(in srgb, var(--primary) 85%, black));
}
}
// Secondary/Outlined variant
&.is-outlined {
background-color: var(--scheme-main);
color: var(--grey-900);
&:hover {
color: var(--grey-600);
}
}
// Tertiary/Text variant
&.is-text {
background-color: transparent;
color: var(--text);
box-shadow: none;
&:hover {
background-color: var(--grey-100);
box-shadow: none;
}
}
&.is-inverted {
// Used with is-text for tertiary buttons
color: inherit;
}
// Danger modifier - solid filled button (default and primary variant)
&.is-danger {
background-color: var(--danger);
border-color: transparent;
color: var(--button-text-color);
&:hover {
background-color: var(--danger-dark);
border-color: transparent;
}
&:focus,
&:focus-visible {
outline-color: var(--danger);
&:not(:active) {
box-shadow: 0 0 0 0.125em hsla(var(--danger-h), var(--danger-s), var(--danger-l), 0.25);
}
}
&:active {
background-color: var(--danger-dark);
border-color: transparent;
}
}
// Danger + outlined/secondary variant
&.is-danger.is-outlined {
background-color: transparent;
border: 1px solid var(--danger);
color: var(--danger);
&:hover,
&:focus {
background-color: var(--danger);
border-color: var(--danger);
color: var(--button-text-color);
}
}
// Danger + text/tertiary variant
&.is-danger.is-text {
background-color: transparent;
color: var(--danger);
&:hover {
background-color: hsla(var(--danger-h), var(--danger-s), var(--danger-l), 0.1);
}
}
// Danger loading spinner - white on solid, danger-colored on outlined/text
&.is-danger.is-loading::after {
border-color: transparent transparent var(--white) var(--white);
}
&.is-danger.is-outlined.is-loading::after,
&.is-danger.is-text.is-loading::after {
border-color: transparent transparent var(--danger) var(--danger);
}
// Loading state
&.is-loading {
color: transparent !important;
pointer-events: none;
position: relative;
&::after {
content: "";
position: absolute;
display: block;
block-size: 1em;
inline-size: 1em;
border: 2px solid var(--button-text-color);
border-radius: 50%;
border-inline-end-color: transparent;
border-block-start-color: transparent;
animation: spin-around 500ms infinite linear;
// Center the spinner
inset-inline-start: calc(50% - 0.5em);
inset-block-start: calc(50% - 0.5em);
}
}
&.is-outlined.is-loading::after,
&.is-text.is-loading::after {
border-color: var(--grey-700);
border-inline-end-color: transparent;
border-block-start-color: transparent;
}
}
@keyframes spin-around {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.is-small {
border-radius: $radius;
}
.underline-none {
text-decoration: none !important;
}
</style>