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:
@@ -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,
|
||||
])
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user