Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ cli/vendor/
dist
dist-hosted
dist-local
dist-marketing
# …except the data-liberation plugin's committed MCP server bundle (the plugin
# installer copies the package without running npm install or a build). The
# package-level .gitignore keeps everything else in its dist/ ignored.
Expand Down
12 changes: 12 additions & 0 deletions apps/ui/index.marketing.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Studio Marketing Screenshot</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.marketing.tsx"></script>
</body>
</html>
5 changes: 4 additions & 1 deletion apps/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@
"dev": "vite",
"dev:hosted": "STUDIO_TARGET=hosted vite",
"dev:local": "STUDIO_TARGET=local vite",
"dev:marketing": "STUDIO_TARGET=marketing vite",
"build": "vite build",
"build:hosted": "STUDIO_TARGET=hosted vite build",
"build:local": "STUDIO_TARGET=local vite build",
"build:marketing": "STUDIO_TARGET=marketing vite build",
"lint": "eslint src",
"typecheck": "tsc -p tsconfig.json --noEmit",
"preview": "vite preview",
"preview:hosted": "STUDIO_TARGET=hosted vite preview",
"preview:local": "STUDIO_TARGET=local vite preview"
"preview:local": "STUDIO_TARGET=local vite preview",
"preview:marketing": "STUDIO_TARGET=marketing vite preview"
},
"dependencies": {
"@base-ui/react": "^1.3.0",
Expand Down
293 changes: 293 additions & 0 deletions apps/ui/public-marketing/marketing-preview/meridian/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
<!doctype html>
<html lang="en" data-marketing-preview-ready="true">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Meridian Coffee</title>
<style>
:root {
color: #173f35;
background: #f5f0e5;
font-family: Arial, Helvetica, sans-serif;
font-synthesis: none;
}

* {
box-sizing: border-box;
}

body {
min-width: 320px;
margin: 0;
background: #f5f0e5;
}

header {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 72px;
padding: 0 5vw;
color: #fffaf0;
background: #173f35;
}

.brand {
display: flex;
align-items: center;
gap: 12px;
font-size: 15px;
font-weight: 700;
letter-spacing: 0.16em;
}

.brand-mark {
display: grid;
width: 32px;
height: 32px;
place-items: center;
border-radius: 50%;
color: #173f35;
background: #e6be75;
font-family: Georgia, serif;
font-size: 19px;
}

nav {
display: flex;
gap: 28px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.1em;
}

.hero {
position: relative;
display: grid;
min-height: min( 610px, 70vh );
overflow: hidden;
grid-template-columns: minmax( 0, 1.05fr ) minmax( 240px, 0.95fr );
background: #cf8b56;
}

.hero::after {
position: absolute;
right: -10%;
bottom: -32%;
width: 72%;
aspect-ratio: 1;
border-radius: 50%;
background: #dfa476;
content: '';
}

.hero-copy {
position: relative;
z-index: 1;
align-self: center;
padding: clamp( 52px, 8vw, 110px ) 5vw;
}

.eyebrow {
margin: 0 0 22px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
}

h1 {
max-width: 680px;
margin: 0;
color: #fffaf0;
font-family: Georgia, 'Times New Roman', serif;
font-size: clamp( 48px, 6.4vw, 94px );
font-weight: 700;
letter-spacing: -0.045em;
line-height: 0.95;
}

.intro {
max-width: 500px;
margin: 28px 0 32px;
color: #fff3df;
font-family: Georgia, 'Times New Roman', serif;
font-size: clamp( 17px, 1.8vw, 24px );
line-height: 1.45;
}

.cta {
display: inline-flex;
align-items: center;
min-height: 48px;
padding: 0 24px;
border-radius: 999px;
color: #fffaf0;
background: #173f35;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-decoration: none;
text-transform: uppercase;
}

.product-stage {
position: relative;
z-index: 1;
display: grid;
place-items: center;
}

.sun {
position: absolute;
top: 12%;
right: 11%;
width: clamp( 120px, 16vw, 230px );
aspect-ratio: 1;
border-radius: 50%;
background: #efd7ad;
}

.bag {
position: relative;
z-index: 1;
width: min( 58%, 330px );
min-width: 190px;
padding: 24px 22px 32px;
transform: rotate( 3deg );
box-shadow: 0 28px 55px rgb( 70 40 24 / 24% );
background: #f6ead1;
text-align: center;
}

.bag::before {
display: block;
height: 11px;
margin: -17px -14px 28px;
background: repeating-linear-gradient( 90deg, #173f35 0 8px, transparent 8px 15px );
content: '';
}

.bag-kicker {
font-size: 9px;
font-weight: 700;
letter-spacing: 0.14em;
}

.bag h2 {
margin: 25px 0 8px;
font-family: Georgia, serif;
font-size: clamp( 30px, 4vw, 53px );
line-height: 0.95;
}

.origin {
margin: 24px 0 0;
border-top: 1px solid #bcae91;
padding-top: 18px;
font-size: 11px;
letter-spacing: 0.11em;
text-transform: uppercase;
}

.principles {
display: grid;
grid-template-columns: repeat( 3, 1fr );
}

.principles article {
min-height: 180px;
padding: 34px 5vw;
background: #fffaf0;
}

.principles article:nth-child( 2 ) {
background: #e7dfcd;
}

.principles article:nth-child( 3 ) {
color: #fffaf0;
background: #173f35;
}

.number {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.13em;
}

.principles h3 {
margin: 32px 0 0;
font-family: Georgia, serif;
font-size: clamp( 20px, 2vw, 30px );
}

@media ( max-width: 720px ) {
nav {
display: none;
}

.hero {
grid-template-columns: 1fr;
}

.hero-copy {
padding-bottom: 28px;
}

.product-stage {
min-height: 410px;
}

.sun {
top: 4%;
}

.principles {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<header>
<div class="brand"><span class="brand-mark">M</span> Meridian Coffee</div>
<nav aria-label="Primary">
<span>Our coffee</span><span>Our story</span><span>Visit</span>
</nav>
</header>
<main>
<section class="hero">
<div class="hero-copy">
<p class="eyebrow">Coffee, roasted with intention</p>
<h1>Make room for a better morning.</h1>
<p class="intro">Small-batch coffee with a warm welcome, roasted fresh every week.</p>
<a class="cta" href="#coffee">Shop coffee</a>
</div>
<div class="product-stage" aria-hidden="true">
<div class="sun"></div>
<div class="bag">
<span class="bag-kicker">MERIDIAN</span>
<h2>Morning<br />Light</h2>
<p>Honey · Cocoa · Citrus</p>
<p class="origin">Single origin · Colombia</p>
</div>
</div>
</section>
<section class="principles" id="coffee">
<article>
<span class="number">01 · Small-batch</span>
<h3>Roasted every week</h3>
</article>
<article>
<span class="number">02 · Responsibly sourced</span>
<h3>Built on relationships</h3>
</article>
<article>
<span class="number">03 · Thoughtfully delivered</span>
<h3>Free shipping over $40</h3>
</article>
</section>
</main>
</body>
</html>
13 changes: 13 additions & 0 deletions apps/ui/src/components/user-menu/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,19 @@ describe( 'UserMenu', () => {
expect( navigateMock ).toHaveBeenCalledWith( { to: '/settings' } );
} );

it( 'shows the Settings icon when a signed-in user has no email', () => {
useAuthUserMock.mockReturnValue( {
data: { id: 1, displayName: 'Ada Lovelace', email: '' },
} as never );

render( <UserMenu onToggleSidebar={ onToggleSidebar } /> );

expect( screen.queryByTestId( 'gravatar' ) ).not.toBeInTheDocument();
expect(
screen.getByRole( 'button', { name: 'Settings' } ).querySelector( 'svg' )
).toBeInTheDocument();
} );

it( 'toggles the sidebar from the hide control', () => {
render( <UserMenu onToggleSidebar={ onToggleSidebar } /> );

Expand Down
3 changes: 2 additions & 1 deletion apps/ui/src/components/user-menu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export function UserMenu( { onToggleSidebar }: Props ) {
const { data: user } = useAuthUser();
const navigate = useNavigate();
const themeIsDark = useColorScheme() === 'dark';
const hasGravatar = Boolean( user?.email.trim() );

return (
<div className={ styles.root }>
Expand All @@ -25,7 +26,7 @@ export function UserMenu( { onToggleSidebar }: Props ) {
className={ styles.userTrigger }
onClick={ () => void navigate( { to: '/settings' } ) }
>
{ user ? (
{ hasGravatar && user ? (
<Gravatar email={ user.email } isDark={ themeIsDark } />
) : (
<span className={ styles.settingsAvatar } aria-hidden="true">
Expand Down
Loading
Loading