feat: fixed footer with responsive height tokens
Footer is fixed bottom-0 with h-footer (5rem mobile) / md:h-footer-wide (4rem desktop). Body gets matching pb-footer/md:pb-footer-wide to reserve space. Tokens registered in @theme inline as --spacing-footer*.
This commit is contained in:
+1
-1
@@ -14,7 +14,7 @@ export const metadata: Metadata = {
|
|||||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={`${fraunces.variable} ${publicSans.variable} flex flex-col min-h-screen`}>
|
<body className={`${fraunces.variable} ${publicSans.variable} pb-footer md:pb-footer-wide`}>
|
||||||
{children}
|
{children}
|
||||||
<Footer />
|
<Footer />
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+17
-12
@@ -84,7 +84,6 @@
|
|||||||
/* === GRID === */
|
/* === GRID === */
|
||||||
--grid-gap: var(--space-3);
|
--grid-gap: var(--space-3);
|
||||||
--section-content-width: 72rem;
|
--section-content-width: 72rem;
|
||||||
|
|
||||||
/* === ANIMATION === */
|
/* === ANIMATION === */
|
||||||
--ease-default: ease;
|
--ease-default: ease;
|
||||||
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
@@ -121,6 +120,8 @@
|
|||||||
--radius-md: var(--radius);
|
--radius-md: var(--radius);
|
||||||
--radius-lg: var(--radius);
|
--radius-lg: var(--radius);
|
||||||
--container-section: var(--section-content-width);
|
--container-section: var(--section-content-width);
|
||||||
|
--spacing-footer: 5rem;
|
||||||
|
--spacing-footer-wide: 4rem;
|
||||||
--text-section-title: var(--text-section-title);
|
--text-section-title: var(--text-section-title);
|
||||||
|
|
||||||
--shadow-brutal-xs: var(--shadow-brutal-xs);
|
--shadow-brutal-xs: var(--shadow-brutal-xs);
|
||||||
@@ -226,12 +227,6 @@
|
|||||||
a {
|
a {
|
||||||
color: var(--blue);
|
color: var(--blue);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border-bottom: 2px solid var(--blue);
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
border-bottom-width: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
@@ -258,19 +253,19 @@
|
|||||||
.brutal-shadow-lg {
|
.brutal-shadow-lg {
|
||||||
box-shadow: var(--shadow-brutal-lg);
|
box-shadow: var(--shadow-brutal-lg);
|
||||||
}
|
}
|
||||||
.brutal-border {
|
@utility brutal-border {
|
||||||
border: var(--border-width) solid var(--blue);
|
border: var(--border-width) solid var(--blue);
|
||||||
}
|
}
|
||||||
.brutal-border-top {
|
@utility brutal-border-top {
|
||||||
border-top: var(--border-width) solid var(--blue);
|
border-top: var(--border-width) solid var(--blue);
|
||||||
}
|
}
|
||||||
.brutal-border-bottom {
|
@utility brutal-border-bottom {
|
||||||
border-bottom: var(--border-width) solid var(--blue);
|
border-bottom: var(--border-width) solid var(--blue);
|
||||||
}
|
}
|
||||||
.brutal-border-left {
|
@utility brutal-border-left {
|
||||||
border-left: var(--border-width) solid var(--blue);
|
border-left: var(--border-width) solid var(--blue);
|
||||||
}
|
}
|
||||||
.brutal-border-right {
|
@utility brutal-border-right {
|
||||||
border-right: var(--border-width) solid var(--blue);
|
border-right: var(--border-width) solid var(--blue);
|
||||||
}
|
}
|
||||||
/* Apply Fraunces variable axes to non-heading elements using the heading font */
|
/* Apply Fraunces variable axes to non-heading elements using the heading font */
|
||||||
@@ -300,6 +295,16 @@
|
|||||||
text-wrap: pretty;
|
text-wrap: pretty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rich-text a {
|
||||||
|
border-bottom: var(--border-width) solid var(--blue);
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity var(--duration-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text a:hover {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
.rich-text p + p {
|
.rich-text p + p {
|
||||||
margin-top: 1.2em;
|
margin-top: 1.2em;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { SiteSettingsRecord } from '$shared/api';
|
import type { SiteSettingsRecord } from '$shared/api';
|
||||||
import { getFirstRecord } from '$shared/api';
|
import { getFirstRecord } from '$shared/api';
|
||||||
import { buildFileUrl } from '$shared/lib';
|
import { buildFileUrl } from '$shared/lib';
|
||||||
import { Button, Link } from '$shared/ui';
|
import { Button, InlineSvg, Link } from '$shared/ui';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Site-wide footer with contact email, social links, and CV download.
|
* Site-wide footer with contact email, social links, and CV download.
|
||||||
@@ -9,21 +9,23 @@ import { Button, Link } from '$shared/ui';
|
|||||||
*/
|
*/
|
||||||
export async function Footer() {
|
export async function Footer() {
|
||||||
const settings = await getFirstRecord<SiteSettingsRecord>('site_settings', {
|
const settings = await getFirstRecord<SiteSettingsRecord>('site_settings', {
|
||||||
expand: 'contacts,contacts.socials',
|
expand: 'contacts,contacts.email,contacts.socials',
|
||||||
tags: ['site_settings'],
|
tags: ['site_settings'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const cvUrl = settings?.cv ? buildFileUrl(settings.collectionId, settings.id, settings.cv) : null;
|
const cvUrl = settings?.cv ? buildFileUrl(settings.collectionId, settings.id, settings.cv) : null;
|
||||||
const contacts = settings?.expand?.contacts;
|
const contacts = settings?.expand?.contacts;
|
||||||
|
const email = contacts?.expand?.email;
|
||||||
const socials = contacts?.expand?.socials ?? [];
|
const socials = contacts?.expand?.socials ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="brutal-border-top px-8 py-6 lg:px-16 lg:py-8">
|
<footer className="fixed bottom-0 left-0 right-0 z-50 h-footer md:h-footer-wide brutal-border-top bg-background px-8 lg:px-16 flex items-center">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
<div className="w-full flex flex-row justify-between gap-4">
|
||||||
<div className="flex flex-wrap items-center gap-4">
|
<div className="flex flex-wrap items-center gap-6 sm:gap-4">
|
||||||
{contacts?.email && (
|
{email && (
|
||||||
<Link href={`mailto:${contacts.email}`} className="text-sm opacity-60 hover:opacity-100 no-underline">
|
<Link href={email.url} external variant="secondary" className="flex items-center gap-1.5 text-sm">
|
||||||
{contacts.email}
|
{email.icon && <InlineSvg svg={email.icon} className="inline-flex w-8 h-8 sm:hidden" />}
|
||||||
|
<span className="hidden sm:block">{email.label}</span>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{socials.map((social) => (
|
{socials.map((social) => (
|
||||||
@@ -31,9 +33,11 @@ export async function Footer() {
|
|||||||
key={social.id}
|
key={social.id}
|
||||||
href={social.url}
|
href={social.url}
|
||||||
external
|
external
|
||||||
className="text-sm opacity-60 hover:opacity-100 no-underline"
|
variant="secondary"
|
||||||
|
className="flex items-center gap-1.5 text-sm"
|
||||||
>
|
>
|
||||||
{social.label}
|
{social.icon && <InlineSvg svg={social.icon} className="inline-flex w-8 h-8 sm:hidden" />}
|
||||||
|
<span className="hidden sm:block">{social.label}</span>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user