feat(website): responsive / mobile layouts
Make the marketing site mobile-friendly across every page and section. Desktop appearance is unchanged; all breakpoint logic targets <=768px. - Fluid section padding via clamp(); equal grids use auto-fit/minmax, asymmetric grids stack to one column via scoped-CSS media queries - Nav: real hamburger menu on mobile (links, lang toggle, login, CTA) - ProductMockup: scales the whole dashboard to fit (zoom) instead of reflowing its internals into a tall stack - Lower oversized heading clamp() minimums so titles no longer overflow at ~390px (hero, page headers, final CTA, brand cover/chapter) - HowItWorks: row-gap when steps stack so node markers clear the text - Compare + partners tables: stacked rows now label each value with its column (Dezky vs hyperscaler / CSP) instead of an ambiguous header - Footer columns, tiers, calculator and tables stack cleanly on mobile
This commit is contained in:
@@ -2,13 +2,47 @@
|
||||
// Stylized customer-portal dashboard shown under the hero. Illustrative only —
|
||||
// labels are intentionally Danish in both languages. Ported from
|
||||
// landing-sections.jsx ProductMockup (light mode).
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { C } from '~/utils/landingTokens'
|
||||
import { useTheme, useDark } from '~/composables/useLanding'
|
||||
|
||||
const t = useTheme()
|
||||
const dark = useDark()
|
||||
|
||||
// The mockup is a fixed-layout dashboard. On desktop it runs fluid/full-width;
|
||||
// when the available width drops below its comfortable design width we scale
|
||||
// the WHOLE thing down (CSS zoom reflows, so no height hacks) instead of
|
||||
// reflowing the internal dashboard — keeps it a recognisable mini-dashboard on
|
||||
// phones. SSR-safe: renders at zoom 1 on the server, recomputes after mount.
|
||||
const DESIGN_W = 760
|
||||
const frame = ref<HTMLElement | null>(null)
|
||||
const zoom = ref(1)
|
||||
const frameWidth = ref('100%')
|
||||
let ro: ResizeObserver | null = null
|
||||
|
||||
function recompute() {
|
||||
const parent = frame.value?.parentElement
|
||||
if (!parent) return
|
||||
const w = parent.clientWidth
|
||||
if (w >= DESIGN_W) {
|
||||
zoom.value = 1
|
||||
frameWidth.value = '100%'
|
||||
} else {
|
||||
frameWidth.value = `${DESIGN_W}px`
|
||||
zoom.value = Math.max(0.4, w / DESIGN_W)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
recompute()
|
||||
const parent = frame.value?.parentElement
|
||||
if (parent && typeof ResizeObserver !== 'undefined') {
|
||||
ro = new ResizeObserver(recompute)
|
||||
ro.observe(parent)
|
||||
}
|
||||
})
|
||||
onBeforeUnmount(() => ro?.disconnect())
|
||||
|
||||
const m = computed(() => ({
|
||||
bg: dark.value ? '#171715' : '#FFFFFF',
|
||||
border: dark.value ? 'rgba(255,255,255,0.08)' : 'rgba(10,10,10,0.08)',
|
||||
@@ -39,10 +73,11 @@ const recent: [string, string, string][] = [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :style="{
|
||||
<div ref="frame" :style="{
|
||||
background: m.bg, border: `1px solid ${m.border}`, borderRadius: '8px',
|
||||
boxShadow: dark ? '0 40px 80px rgba(0,0,0,0.5)' : '0 30px 80px rgba(10,10,10,0.08)',
|
||||
overflow: 'hidden',
|
||||
width: frameWidth, zoom,
|
||||
}">
|
||||
<!-- Window chrome -->
|
||||
<div :style="{ display: 'flex', alignItems: 'center', gap: '8px', padding: '14px 18px', borderBottom: `1px solid ${m.border}` }">
|
||||
@@ -52,7 +87,7 @@ const recent: [string, string, string][] = [
|
||||
<div :style="{ marginLeft: '16px', padding: '4px 12px', background: m.subtle, borderRadius: '4px', fontFamily: '\'JetBrains Mono\', monospace', fontSize: '11px', color: m.muted }">app.dezky.eu / dashboard</div>
|
||||
</div>
|
||||
|
||||
<div :style="{ display: 'grid', gridTemplateColumns: '220px 1fr', minHeight: '460px' }">
|
||||
<div class="mockup-body" :style="{ display: 'grid', minHeight: '460px' }">
|
||||
<!-- Sidebar -->
|
||||
<div :style="{ borderRight: `1px solid ${m.border}`, padding: '20px 0' }">
|
||||
<div :style="{ display: 'flex', alignItems: 'center', gap: '8px', padding: '0 20px', marginBottom: '24px' }">
|
||||
@@ -84,8 +119,8 @@ const recent: [string, string, string][] = [
|
||||
</div>
|
||||
|
||||
<!-- Main -->
|
||||
<div :style="{ padding: '28px 32px' }">
|
||||
<div :style="{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }">
|
||||
<div :style="{ padding: 'clamp(16px, 4vw, 28px) clamp(16px, 4vw, 32px)' }">
|
||||
<div :style="{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', flexWrap: 'wrap', gap: '8px' }">
|
||||
<div>
|
||||
<div :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '11px', color: m.muted, letterSpacing: '0.1em', textTransform: 'uppercase' }">indbakke</div>
|
||||
<div :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontWeight: 600, fontSize: '28px', color: m.fg, marginTop: '6px', letterSpacing: '-0.02em' }">god morgen, anne</div>
|
||||
@@ -95,7 +130,7 @@ const recent: [string, string, string][] = [
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :style="{ marginTop: '24px', display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '12px' }">
|
||||
<div :style="{ marginTop: '24px', display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 120px), 1fr))', gap: '12px' }">
|
||||
<div v-for="(s, i) in stats" :key="i" :style="{ padding: '16px 18px', background: m.subtle, borderRadius: '4px' }">
|
||||
<div :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '10px', color: m.muted, letterSpacing: '0.1em', textTransform: 'uppercase' }">{{ s[2] }}</div>
|
||||
<div :style="{ fontFamily: '\'Inter Tight\', sans-serif', fontSize: '28px', fontWeight: 600, color: m.fg, marginTop: '4px' }">{{ s[1] }}</div>
|
||||
@@ -107,8 +142,8 @@ const recent: [string, string, string][] = [
|
||||
<div :style="{ fontFamily: '\'JetBrains Mono\', monospace', fontSize: '10px', color: m.muted, letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: '12px' }">seneste · indbakke</div>
|
||||
<div
|
||||
v-for="(r, i) in recent" :key="i"
|
||||
class="mockup-mail-row"
|
||||
:style="{
|
||||
display: 'grid', gridTemplateColumns: '200px 1fr 60px',
|
||||
padding: '12px 0', borderTop: i === 0 ? `1px solid ${m.border}` : 'none',
|
||||
borderBottom: `1px solid ${m.border}`,
|
||||
fontFamily: '\'Inter\', sans-serif', fontSize: '13px', alignItems: 'center',
|
||||
@@ -123,3 +158,15 @@ const recent: [string, string, string][] = [
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Fixed dashboard layout — the whole mockup is scaled to fit on small screens
|
||||
(see the zoom logic in <script>), so the internal grid stays desktop-shaped. */
|
||||
.mockup-body {
|
||||
grid-template-columns: 220px 1fr;
|
||||
}
|
||||
.mockup-mail-row {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr 60px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user