mirror of
https://github.com/modrinth/code.git
synced 2026-08-26 09:34:50 +00:00
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:
@@ -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>
|
||||
Reference in New Issue
Block a user