refactor: Button components (#6929)

* feat: refactor button components

* fix: qa

* fix: storybook

* a11y: pass

* fix: build

* fix: rename default ->md

* fix: lint

* docs: buttons.md

* refactor part 1

* refactor part 2

* fix: undo refactor for fresh restart

* refactor

* Revert "refactor"

This reverts commit 96d65902d7.

* refactor: part 1

* fix: qa

* fix: qa

* fix: qa

* fix: qa

* fix: qa

* fix: qa

* fix: qa

* fix: qa

* fix: remove text-contrast

* fix: qa

* fix: v-tooltip

* fix: lint

* fix: split broken

* fix: passkey qa

* fix: qa

* fix: qa

* fix: prepr

* fix: splitbutton

* improve button group seam

---------

Signed-off-by: Calum H. <calum@modrinth.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
Calum H.
2026-08-04 13:54:04 -07:00
committed by GitHub
co-authored by Prospector
parent 6cfe999cb3
commit bb2193b6f5
398 changed files with 13688 additions and 13133 deletions
@@ -0,0 +1,165 @@
<script setup lang="ts">
import type { Component, CSSProperties } from 'vue'
import { computed, ref } from 'vue'
import type {
ButtonColor,
ButtonInteraction,
ButtonNativeType,
ButtonSize,
ButtonType,
} from './types'
const baseClasses = [
// Base
'relative inline-flex min-w-0 shrink-0 items-center justify-center',
'whitespace-nowrap border-0 no-underline',
// Interactions
'touch-manipulation cursor-pointer select-none transition-[background-color,color,box-shadow,filter,opacity,transform] duration-150 ease-out',
'enabled:active:scale-[0.97]',
// Hovering
'[&:not(:disabled):not([aria-disabled=true]):hover]:brightness-[--hover-brightness]',
// Accessibility
'[&:not(:disabled):not([aria-disabled=true]):focus-visible]:brightness-[--hover-brightness] focus-visible:outline-none [&:not(:disabled):not([aria-disabled=true]):focus-visible]:ring-4 [&:not(:disabled):not([aria-disabled=true]):focus-visible]:ring-brand-shadow',
'disabled:cursor-not-allowed disabled:opacity-50',
'[&[aria-disabled=true]]:cursor-not-allowed [&[aria-disabled=true]]:opacity-50',
].join(' ')
const sizeClasses: Record<ButtonSize, string> = {
xs: 'h-7 gap-1 rounded-lg px-1.5 text-sm font-semibold leading-5 [&>svg]:size-4 [&>svg]:min-h-4 [&>svg]:min-w-4 [&>svg]:shrink-0',
sm: 'h-8 gap-1 rounded-[10px] px-1.5 text-sm font-semibold leading-5 [&>svg]:size-4 [&>svg]:min-h-4 [&>svg]:min-w-4 [&>svg]:shrink-0',
md: 'h-9 gap-1.5 rounded-xl px-2.5 text-base font-semibold leading-5 [&>svg]:size-5 [&>svg]:min-h-5 [&>svg]:min-w-5 [&>svg]:shrink-0',
lg: 'h-10 gap-2 rounded-[14px] px-4 text-base font-semibold leading-5 [&>svg]:size-5 [&>svg]:min-h-5 [&>svg]:min-w-5 [&>svg]:shrink-0',
xl: 'h-12 gap-2 rounded-2xl px-3.5 text-base font-extrabold leading-5 [&>svg]:size-6 [&>svg]:min-h-6 [&>svg]:min-w-6 [&>svg]:shrink-0',
}
const iconOnlySizeClasses: Record<ButtonSize, string> = {
xs: 'w-7 !px-0',
sm: 'w-8 !px-0',
md: 'w-9 !px-0',
lg: 'w-10 !px-0',
xl: 'w-12 !px-0',
}
const typeClasses: Record<ButtonType, string> = {
base: 'button-frame--base bg-surface-4 text-contrast [&>svg]:text-primary',
colored:
'button-frame--colored bg-[--button-color] text-[var(--color-accent-contrast)] [&>svg]:text-inherit',
outlined:
'button-frame--outlined bg-transparent text-[var(--button-color,var(--color-contrast))] [&>svg]:text-inherit',
quiet: 'button-frame--quiet bg-transparent [&>svg]:text-inherit',
}
const interactionClasses: Record<ButtonInteraction, string> = {
surface:
'[&:not(:disabled):not([aria-disabled=true]):hover]:bg-surface-4 [&:not(:disabled):not([aria-disabled=true]):focus-visible]:bg-surface-4',
filled:
'[&:not(:disabled):not([aria-disabled=true]):hover]:!bg-[--button-color] [&:not(:disabled):not([aria-disabled=true]):focus-visible]:!bg-[--button-color] [&:not(:disabled):not([aria-disabled=true]):hover]:!text-[var(--color-accent-contrast)] [&:not(:disabled):not([aria-disabled=true]):focus-visible]:!text-[var(--color-accent-contrast)]',
none: '[&:not(:disabled):not([aria-disabled=true]):hover]:!brightness-100 [&:not(:disabled):not([aria-disabled=true]):focus-visible]:!brightness-100',
}
const colorVariables: Record<ButtonColor, string> = {
brand: 'var(--color-brand)',
red: 'var(--color-red)',
orange: 'var(--color-orange)',
green: 'var(--color-green)',
blue: 'var(--color-blue)',
purple: 'var(--color-purple)',
medal_promotion: 'var(--medal-promotion-text-orange, var(--color-orange))',
}
const props = withDefaults(
defineProps<{
as: string | Component
type?: ButtonType
color?: ButtonColor
size?: ButtonSize
interaction?: ButtonInteraction
iconOnly?: boolean
circular?: boolean
nativeType?: ButtonNativeType
}>(),
{
type: 'base',
size: 'md',
interaction: 'surface',
iconOnly: false,
circular: false,
nativeType: undefined,
},
)
const element = ref<HTMLElement | null>(null)
const classes = computed(() => [
baseClasses,
typeClasses[props.type],
props.type === 'quiet' ? interactionClasses[props.interaction] : '',
sizeClasses[props.size],
props.iconOnly ? iconOnlySizeClasses[props.size] : '',
props.circular ? '!rounded-full' : '',
])
const style = computed((): CSSProperties | undefined => {
if ((props.type === 'outlined' || props.type === 'quiet') && !props.color) return undefined
if (props.type !== 'colored' && props.type !== 'outlined' && props.type !== 'quiet')
return undefined
return {
'--button-color': colorVariables[props.color ?? 'brand'],
} as CSSProperties
})
defineExpose({ element })
</script>
<template>
<component
:is="as"
ref="element"
data-button
:type="props.nativeType"
:class="classes"
:style="style"
>
<slot />
</component>
</template>
<style scoped>
.button-frame--base {
box-shadow:
inset 0 0 0 1px var(--surface-5),
0 1px 1px rgba(0, 0, 0, 0.12);
}
.button-frame--colored {
box-shadow:
0 0 0 1px color-mix(in srgb, var(--button-color) 30%, transparent),
0 2px 4px rgba(0, 0, 0, 0.04),
0 5px 8px rgba(0, 0, 0, 0.04),
0 10px 18px rgba(0, 0, 0, 0.03),
0 24px 48px rgba(0, 0, 0, 0.03);
}
.button-frame--colored::before {
position: absolute;
inset: 0;
padding: 1px;
pointer-events: none;
content: '';
border-radius: inherit;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0));
-webkit-mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
}
.button-frame--outlined {
box-shadow: inset 0 0 0 1px var(--button-color, var(--surface-5));
}
.button-frame--quiet {
color: var(--button-color, var(--color-base));
}
</style>