funkwhale/front/src/components/ui/Button.vue

267 lines
6.3 KiB
Vue

<script setup lang="ts">
import { ref, computed, useSlots, onMounted } from 'vue'
import { type ColorProps, type VariantProps, type DefaultProps, type RaisedProps, type PastelProps, color } from '~/composables/color';
import { type WidthProps, width } from '~/composables/width'
import { type AlignmentProps, align } from '~/composables/alignment'
import Loader from '~/components/ui/Loader.vue'
const props = defineProps<{
thinFont?: true
lowHeight? : true
isActive?: boolean
isLoading?: boolean
shadow?: boolean
round?: boolean
icon?: string | `right ${string}`
onClick?: (...args: any[]) => void | Promise<void> // The default fallback is `submit`
split?: boolean // Add this prop for split button support
splitIcon?: string // Add this prop for the split button icon
splitTitle?: string // Add this prop
onSplitClick?: (...args: any[]) => void | Promise<void> // Add click handler for split part
dropdownOnly?: boolean
disabled?: boolean
autofocus? : boolean
ariaPressed? : true
} & (ColorProps | DefaultProps | PastelProps )
& VariantProps
& RaisedProps
& WidthProps
& AlignmentProps>()
const slots = useSlots()
// TODO: Refactor this once upload button progress indicator can be tested (in Sidebar.vue)
const isIconOnly = computed(() => !!props.icon && (!slots.default || 'square' in props && props.square || 'squareSmall' in props && props.squareSmall ))
const isSplitIconOnly = computed(() => !!props.splitIcon && !props.splitTitle)
const internalLoader = ref(false)
const isLoading = computed(() => props.isLoading || internalLoader.value)
const fontWeight = props.thinFont ? 400 : 900
const button = ref()
const click = async (...args: any[]) => {
internalLoader.value = true
try {
await props.onClick?.(...args)
} finally {
internalLoader.value = false
}
}
onMounted(() => {
if (props.autofocus) button.value.focus();
})
</script>
<template>
<div v-if="split" class="funkwhale split-button">
<button
v-if="!dropdownOnly"
ref="button"
v-bind="{
...$attrs,
...color(props, ['interactive'])(
width(props, isIconOnly ? ['square'] : ['normalHeight', 'buttonWidth'])(
align(props, { alignText:'center' })(
)))}"
class="funkwhale button split-main"
:autofocus="autofocus || undefined"
:disabled="disabled || undefined"
:aria-pressed="props.ariaPressed"
:class="{
'is-loading': isLoading,
'is-icon-only': isIconOnly,
'has-icon': !!icon,
'is-round': round,
'is-shadow': shadow,
}"
@click="click"
>
<slot name="main">
<i v-if="icon && !icon.startsWith('right ')" :class="['bi', icon]" />
<span v-if="!isIconOnly">
<slot />
</span>
<i v-if="icon && icon.startsWith('right ')" :class="['bi', icon.replace('right ', '')]" />
</slot>
<Loader v-if="isLoading" :container="false" />
</button>
<button
v-bind="{
...$attrs,
...color(props, ['interactive'])(
width(props, isSplitIconOnly ? ['square'] : ['normalHeight', 'buttonWidth'])(
align(props, { alignSelf:'start', alignText:'center' })(
)))}"
:disabled="disabled || undefined"
:autofocus="autofocus || undefined"
:class="[
'funkwhale',
'button',
{
'split-toggle': true,
'is-loading': isLoading,
'is-icon-only': isSplitIconOnly,
'has-icon': !!splitIcon,
'is-round': round,
'is-shadow': shadow
}
]"
@click="onSplitClick"
>
<span v-if="splitTitle">{{ splitTitle }}</span>
<i :class="['bi', splitIcon]" />
</button>
</div>
<button
v-else
ref="button"
v-bind="color(props, ['interactive'])(
width(props, isIconOnly ? ['square'] : ['normalHeight', 'buttonWidth'])(
align(props, { alignSelf:'start', alignText:'center' })(
)))"
:disabled="disabled || undefined"
:autofocus="autofocus || undefined"
class="funkwhale button"
:aria-pressed="props.ariaPressed"
:class="{
'is-loading': isLoading,
'is-icon-only': isIconOnly,
'has-icon': !!icon,
'is-round': round,
'is-shadow': shadow,
}"
@click="click"
:type="onClick ? 'button' : 'submit' /* Prevents default `submit` if onCLick is set */"
>
<i v-if="icon && !icon.startsWith('right ')" :class="['bi', icon]" />
<span v-if="!isIconOnly">
<slot />
</span>
<i v-if="icon && icon.startsWith('right ')" :class="['bi', icon.replace('right ', '')]" />
<Loader v-if="isLoading" :container="false" />
</button>
</template>
<style lang="scss">
.funkwhale {
&.split-button {
.button {
display: inline-flex; // Ensure consistent display
align-items: center;
&.split-main {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right: 1px solid var(--border-color);
}
&.split-toggle {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
}
&.button {
// Layout
--padding: 16px;
--shift-by: 0.5px;
gap: 8px;
position: relative;
display: inline-flex;
white-space: nowrap;
justify-content: space-between;
align-items: center;
cursor: pointer;
padding: calc(var(--padding) / 2 - var(--shift-by)) var(--padding) calc(var(--padding) / 2 + var(--shift-by)) var(--padding);
&.is-icon-only {
padding: var(--padding);
}
// Font
font-family: $font-main;
font-weight: v-bind(fontWeight);
font-size: 14px;
line-height: 14px;
// Decoration
transform: translateX(var(--fw-translate-x)) translateY(var(--fw-translate-y)) scale(var(--fw-scale));
transition: all .2s ease;
&.is-shadow {
box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.2);
}
border-radius: var(--fw-border-radius);
&.is-round {
border-radius: 100vh;
}
// States
&[disabled] {
font-weight: normal;
pointer-events: none;
}
&.is-loading {
@extend :active;
> span {
opacity: 0;
}
}
// Content
> span {
position: relative;
top: calc(0px - var(--shift-by));
}
// Icon
> i.bi {
font-size: 18px;
margin: -2px 0;
&.large {
font-size: 32px;
margin: -8px 0;
}
}
&.is-icon-only i.bi {
margin: -6px;
&.large {
margin: -8px;
}
}
&:has(>i){
gap: 10px;
}
}
}
</style>