First-sign-in banner + slim-strip variant for returning users.
How this page works: each card below is one mockup, and its drawer is complete on its own — full Vue source, the parent invocation, scoped CSS, and behavior notes. Click View handoff to open it. Each code box is click-to-copy. Both surfaces are light, so the stage flips to a dark dotted ground for contrast.
<WelcomeBanner> — new composition (Vue + CSS below)<script setup>
import PrimaryButton from '@/buttons/PrimaryButton.vue'
import Text from '@/components/Text.vue'
defineProps({
heading: {
type: String,
default: 'Welcome to the EmojiCopy beta',
},
body: {
type: String,
default: 'You\'re one of the first to use the new EmojiCopy — it\'s completely ad-free while we\'re in beta, and Pro is half off for the first 500 sign-ups.',
},
})
const emit = defineEmits(['cta', 'dismiss'])
</script>
<template>
<div class="welcome-banner">
<div class="welcome-banner__icon jp-font">🎉</div>
<div class="welcome-banner__text">
<Text tag="span" class="welcome-banner__heading" type="body/m-heavy" :text="heading" />
<Text tag="span" class="welcome-banner__body" type="body/s" :text="body" />
</div>
<div class="welcome-banner__actions">
<PrimaryButton variant="green" size="md" label="See what's in Pro"
@click="emit('cta')" />
<PrimaryButton variant="gray" size="md" label="Got it"
@click="emit('dismiss')" />
</div>
</div>
</template>
<!-- Parent invocation (top of MyEmoji.vue): -->
<WelcomeBanner v-if="account.firstSeenInBeta && !account.dismissedWelcome"
@cta="router.push('/pricing')"
@dismiss="account.dismissWelcome()" />
<style scoped>
.welcome-banner {
display: flex;
align-items: center;
gap: var(--sp-4);
padding: var(--sp-4) var(--sp-5);
border-radius: var(--r-lg);
background:
radial-gradient(circle at 0% 0%, color-mix(in srgb, var(--green-500) 30%, transparent), transparent 60%),
var(--c-surface-secondary);
border: 1px solid color-mix(in srgb, var(--green-600) 30%, var(--c-border-primary));
}
.welcome-banner__icon {
font-size: 2.4rem;
flex: none;
}
.welcome-banner__text {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.welcome-banner__heading {
font-family: var(--ff-inter);
font-weight: 700;
font-size: 1.5rem;
color: var(--c-text-primary);
}
.welcome-banner__body {
font-family: var(--ff-inter);
font-weight: 400;
font-size: 1.3rem;
color: var(--c-text-secondary);
}
.welcome-banner__actions {
display: flex;
gap: 8px;
align-items: center;
}
</style>
<SlimStrip> — new composition (Vue + CSS below)<script setup>
import Text from '@/components/Text.vue'
import PrimaryButton from '@/buttons/PrimaryButton.vue'
defineProps({
icon: {
type: String,
default: '👑',
},
heading: {
type: String,
default: '50% off Pro for life · founding',
},
body: {
type: String,
default: 'Annual plan. Founding 50% off Pro — for life, for the first 500 sign-ups. Ad-free during beta.',
},
ctaLabel: {
type: String,
default: 'Claim founding',
},
})
const emit = defineEmits(['cta'])
</script>
<template>
<div class="slim-strip">
<div class="slim-strip__container">
<div class="slim-strip__icon jp-font">{{ icon }}</div>
<div class="slim-strip__text">
<Text tag="span" class="slim-strip__heading" type="body/l-heavy" :text="heading" />
<Text tag="span" class="slim-strip__body" type="body/m" :text="body" />
</div>
</div>
<PrimaryButton variant="gray" size="md" :label="ctaLabel"
@click="emit('cta')" />
</div>
</template>
<!-- Parent invocation (top of MyEmoji.vue, after welcome dismissed): -->
<SlimStrip v-if="account.dismissedWelcome"
@cta="router.push('/pricing')" />
<style scoped>
.slim-strip {
display: flex;
align-items: center;
gap: var(--sp-4);
padding: var(--sp-4);
border-radius: var(--r-lg);
background: var(--metal-50);
border: 1px solid var(--metal-100);
width: 100%;
max-width: 1040px;
}
.slim-strip__container {
flex: 1;
display: flex;
align-items: center;
gap: var(--sp-3);
padding-right: var(--sp-4);
background: var(--white);
border: 1px solid var(--metal-200);
border-radius: var(--r-md);
min-height: 56px;
}
.slim-strip__icon {
width: 56px;
height: 56px;
flex: none;
border-radius: var(--r-md);
background: var(--white);
display: grid;
place-items: center;
font-size: 2.4rem;
color: var(--green-600);
box-shadow: 0 16px 72px rgba(23, 26, 87, .12);
}
.slim-strip__text {
display: flex;
flex-direction: column;
gap: 2px;
line-height: 1.3;
}
.slim-strip__heading {
font-family: var(--ff-inter);
font-weight: 700;
font-size: 1.7rem;
color: var(--metal-950);
}
.slim-strip__body {
font-family: var(--ff-inter);
font-weight: 400;
font-size: 1.4rem;
color: var(--metal-950);
}
.slim-strip__cta {
display: inline-flex;
align-items: center;
gap: var(--sp-1);
padding: var(--sp-2) var(--sp-4);
border-radius: var(--r-pill);
background: var(--white);
border: 1px solid var(--metal-100);
font-family: var(--ff-inter);
font-weight: 700;
font-size: 1.4rem;
color: var(--metal-950);
cursor: pointer;
flex: none;
&:hover { background: var(--metal-50); }
}
</style>