refactor(design-system): consolidate cart + sheet-close into IconButton

IconButton adds:
- xs size (36px) for compact chrome
- ghost variant (transparent → brand-wash on hover)
- shadow prop so callers opt into elevation per-instance

Callsites collapsed:
- Navbar desktop + mobile cart buttons now share one IconButton
  (accent variant; mobile uses shadow='md'). CART_CLASS constant
  deleted — the variant owns that coloring now.
- DesignLayout mobile sheet close uses IconButton ghost xs.

Tone-coupled pills left inline by design:
- Navbar desktop search trigger — visual tone bound to navbar variants
- Search overlay input-row close — tone bound to Search's internal tones
- Hero secondary CTA — tone bound to Hero's internal tones
- Navbar overlay menu cart row — compound pill + inline count chip,
  one-off pattern
This commit is contained in:
Dorian
2026-04-21 14:56:09 +01:00
parent 12c41a22b3
commit e60e24ab23
37 changed files with 84 additions and 61 deletions

View File

@@ -15,17 +15,27 @@ const props = defineProps({
* - 'accent' — warm yellow CTA (cart)
* - 'cream-wash' — translucent cream on brand overlays (overlay close)
* - 'brand-wash' — subtle brand tint on light surfaces (compact chrome)
* - 'ghost' — transparent → brand-wash on hover (sheet close)
*/
variant: {
type: String,
default: 'float',
validator: (v) => ['float', 'accent', 'cream-wash', 'brand-wash'].includes(v),
validator: (v) => ['float', 'accent', 'cream-wash', 'brand-wash', 'ghost'].includes(v),
},
/** Button diameter: sm=40px, md=44px, lg=56px. */
/** Button diameter: xs=36px, sm=40px, md=44px, lg=56px. */
size: {
type: String,
default: 'lg',
validator: (v) => ['sm', 'md', 'lg'].includes(v),
validator: (v) => ['xs', 'sm', 'md', 'lg'].includes(v),
},
/**
* Shadow depth. Null uses the variant default (float → lg, others → none).
* Set explicitly when a non-float variant needs elevation (e.g. mobile cart).
*/
shadow: {
type: String,
default: null,
validator: (v) => v === null || ['none', 'sm', 'md', 'lg'].includes(v),
},
/** Optional numeric overlay (cart count, unread, etc.). Hidden when 0/null. */
count: { type: Number, default: null },
@@ -40,13 +50,30 @@ defineEmits(['click'])
const RAISED = new Set(['float', 'accent'])
const variants = {
float: 'bg-brand-float text-accent shadow-lg',
float: 'bg-brand-float text-accent',
accent: 'bg-accent text-brand hover:bg-accent-soft',
'cream-wash': 'bg-cream-wash text-cream hover:bg-cream-wash-strong',
'brand-wash': 'bg-brand-wash text-brand',
ghost: 'bg-transparent text-muted hover:bg-brand-wash hover:text-brand',
}
const defaultShadow = {
float: 'shadow-lg',
accent: '',
'cream-wash': '',
'brand-wash': '',
ghost: '',
}
const shadowClass = {
none: '',
sm: 'shadow-sm',
md: 'shadow-md',
lg: 'shadow-lg',
}
const sizes = {
xs: { box: 'w-9 h-9', icon: 18 },
sm: { box: 'w-10 h-10', icon: 20 },
md: { box: 'w-11 h-11', icon: 20 },
lg: { box: 'w-14 h-14', icon: 22 },
@@ -60,10 +87,15 @@ const motion = computed(() =>
: 'transition-colors',
)
const elevation = computed(() =>
props.shadow === null ? defaultShadow[props.variant] : shadowClass[props.shadow],
)
const classes = computed(() => [
'relative inline-flex items-center justify-center rounded-full',
sizes[props.size].box,
variants[props.variant],
elevation.value,
motion.value,
])

View File

@@ -49,10 +49,6 @@ const { t } = useI18n()
const menuOpen = ref(false)
const searchOpen = ref(false)
// Cart is always warm yellow — one consistent affordance on mobile and desktop,
// regardless of bar tone.
const CART_CLASS = 'bg-accent text-brand hover:bg-accent-soft'
const tones = {
brand: {
bar: 'bg-brand text-cream border-cream-line',
@@ -152,18 +148,14 @@ onBeforeUnmount(() => {
<span>{{ t('search.placeholder') }}</span>
</button>
<LanguageSwitcher :tone="variant" />
<button
type="button"
:class="[CART_CLASS, 'relative inline-flex items-center justify-center w-11 h-11 rounded-full transition-all duration-base ease-out hover:-translate-y-0.5']"
<IconButton
icon="cart"
variant="accent"
size="md"
:count="cartCount"
:aria-label="t('cart.open')"
@click="$emit('cart')"
>
<Icon name="cart" :size="20" />
<span
v-if="cartCount > 0"
class="absolute -top-1 -right-1 min-w-[18px] h-[18px] px-1 rounded-full bg-danger text-white text-[10px] font-bold flex items-center justify-center"
>{{ cartCount }}</span>
</button>
/>
</div>
</div>
@@ -190,18 +182,15 @@ onBeforeUnmount(() => {
class="md:hidden fixed bottom-5 right-5 z-40 flex items-center gap-3"
style="padding-bottom: env(safe-area-inset-bottom);"
>
<button
type="button"
:class="[CART_CLASS, 'relative w-14 h-14 rounded-full shadow-md flex items-center justify-center transition-transform duration-base ease-out hover:-translate-y-0.5 active:translate-y-0']"
<IconButton
icon="cart"
variant="accent"
size="lg"
shadow="md"
:count="cartCount"
:aria-label="t('cart.open')"
@click="$emit('cart')"
>
<Icon name="cart" :size="22" />
<span
v-if="cartCount > 0"
class="absolute -top-1 -right-1 min-w-[18px] h-[18px] px-1 rounded-full bg-danger text-white text-[10px] font-bold flex items-center justify-center"
>{{ cartCount }}</span>
</button>
/>
<IconButton
icon="menu"
variant="float"

View File

@@ -4,6 +4,7 @@ import { RouterLink, RouterView, useRoute } from 'vue-router'
import Logo from '@/design-system/components/Logo.vue'
import LanguageSwitcher from '@/design-system/components/LanguageSwitcher.vue'
import Icon from '@/design-system/components/Icon.vue'
import IconButton from '@/design-system/components/IconButton.vue'
import { useI18n } from '@/i18n/index.js'
const { t } = useI18n()
@@ -141,14 +142,14 @@ watch(() => route.name, closeSheet)
<span class="eyebrow">{{ t('ds.eyebrow.designSystem') }}</span>
<span class="text-[15px] font-semibold">{{ t('ds.mobile.selectPage') }}</span>
</span>
<button
type="button"
class="inline-flex items-center justify-center w-9 h-9 rounded-pill hover:bg-brand-wash text-muted hover:text-brand transition-colors"
<IconButton
icon="close"
variant="ghost"
size="xs"
:icon-size="22"
:aria-label="t('menu.close')"
@click="closeSheet"
>
<Icon name="close" :size="22" />
</button>
/>
</div>
<nav class="flex-1 overflow-y-auto px-3 py-4 space-y-6">