footer-taped
FC-0055footerprimitivev1.0.0Voettekst als een vel dat met twee stukken plakband op de pagina zit: merk + tagline, kolommen met links en 'binnenkort'-labels, daaronder copyright, juridische links en sociale iconen.
blocks/footer-taped/footer-taped.tsx
/**
* FooterTaped — een voettekst als een vel dat met twee stukken plakband op
* de pagina zit: merk + tagline links, kolommen met links (en 'binnenkort'-
* labels voor wat er nog komt), daaronder de regel met copyright, juridische
* links en sociale iconen.
*
* De bron was één bedrijf (ActivationLed) met ingebakken links, lucide-
* iconen en #222222-plakband. Hier: alles data van het project, iconen als
* slot, plakband in currentColor op --foreground. Links via
* `linkComponent` (a, of Next Link).
*/
import type * as React from "react";
export interface FooterLink {
label: string;
href: string;
/** 'binnenkort': niet klikbaar, met een scheef labeltje. */
soon?: boolean;
}
export interface FooterColumn {
title: string;
soon?: boolean;
links: readonly FooterLink[];
}
export interface FooterSocial {
label: string;
href: string;
icon: React.ReactNode;
}
export interface FooterTapedProps extends React.HTMLAttributes<HTMLElement> {
brand: React.ReactNode;
tagline?: React.ReactNode;
columns: readonly FooterColumn[];
copyright?: React.ReactNode;
legal?: readonly FooterLink[];
socials?: readonly FooterSocial[];
soonLabel?: string;
linkComponent?: React.ElementType;
}
/* Het plakband: één vorm, tweemaal geplaatst, in de tekstkleur. */
function Tape({ className }: { className?: string }) {
return (
<svg aria-hidden="true" className={className} viewBox="0 0 95 80" width="80" height="36" fill="none">
<path d="M1 45L70.282 5L88.282 36.177L19 76.177L1 45Z" fill="currentColor" />
</svg>
);
}
export function FooterTaped({ brand, tagline, columns, copyright, legal = [], socials = [], soonLabel = "binnenkort", linkComponent: Link = "a", className, ...rest }: FooterTapedProps) {
const Soon = ({ draai }: { draai: 1 | -1 }) => (
<span className="fp-foot__soon" style={{ transform: `rotate(${3 * draai}deg)` }}>{soonLabel}</span>
);
return (
<footer {...rest} className={["fp-foot", className].filter(Boolean).join(" ")}>
<div className="fp-foot__vel">
<Tape className="fp-foot__tape fp-foot__tape--links" />
<Tape className="fp-foot__tape fp-foot__tape--rechts" />
<div className="fp-foot__merk">
<div className="fp-foot__naam">{brand}</div>
{tagline && <p className="fp-foot__tagline">{tagline}</p>}
</div>
<div className="fp-foot__kolommen">
{columns.map((k, ki) => (
<div key={k.title} className="fp-foot__kolom">
<h4 className="fp-foot__kop">
{k.title}
{k.soon && <Soon draai={ki % 2 ? -1 : 1} />}
</h4>
<ul className="fp-foot__links">
{k.links.map((l, li) => (
<li key={l.label}>
{l.soon || k.soon ? (
<span className="fp-foot__link fp-foot__link--uit">{l.label}{l.soon && <Soon draai={li % 2 ? 1 : -1} />}</span>
) : (
<Link href={l.href} className="fp-foot__link">{l.label}</Link>
)}
</li>
))}
</ul>
</div>
))}
</div>
</div>
<div className="fp-foot__onder">
<div className="fp-foot__onder-links">
{copyright && <p className="fp-foot__copy">{copyright}</p>}
{legal.length > 0 && (
<ul className="fp-foot__legal">
{legal.map((l) => (
<li key={l.label}><Link href={l.href} className="fp-foot__legal-link">{l.label}</Link></li>
))}
</ul>
)}
</div>
{socials.length > 0 && (
<ul className="fp-foot__social">
{socials.map((s) => (
<li key={s.label}>
<a href={s.href} target="_blank" rel="noopener noreferrer" aria-label={s.label} className="fp-foot__social-link">{s.icon}</a>
</li>
))}
</ul>
)}
</div>
</footer>
);
}
blocks/footer-taped/footer-taped.css
/** FooterTaped — importeren in de app-root (registry-site: app/globals.css). */
.fp-foot { width: 100%; max-width: 64rem; margin: 2rem auto; color: var(--foreground); }
.fp-foot__vel {
position: relative;
display: flex; flex-direction: column; gap: 2rem;
padding: 2.5rem 1.5rem;
background: var(--card); color: var(--card-foreground, var(--foreground));
border: 1px solid var(--border); border-radius: calc(var(--radius) * 2);
}
@media (min-width: 768px) { .fp-foot__vel { flex-direction: row; justify-content: space-between; padding: 2.5rem 2.5rem; } }
/* Het plakband: tekstkleur op 85%, schuin over de hoeken. */
.fp-foot__tape { position: absolute; top: -1rem; display: none; color: color-mix(in srgb, var(--foreground) 85%, transparent); }
@media (min-width: 768px) { .fp-foot__tape { display: block; } }
.fp-foot__tape--links { left: -2rem; transform: scale(0.75); }
.fp-foot__tape--rechts { right: -2rem; transform: rotate(90deg) scale(0.75); }
.fp-foot__merk { display: flex; flex-direction: column; gap: 0.5rem; max-width: 20rem; }
.fp-foot__naam { font-size: 1.5rem; font-weight: 800; letter-spacing: -0.01em; }
.fp-foot__tagline { margin: 0; font-weight: 500; color: var(--muted-foreground); }
.fp-foot__kolommen { display: flex; flex-direction: column; gap: 1.5rem; }
@media (min-width: 768px) { .fp-foot__kolommen { flex-direction: row; gap: 4rem; } }
.fp-foot__kolom { display: flex; flex-direction: column; gap: 0.5rem; }
@media (min-width: 768px) { .fp-foot__kolom { gap: 1rem; } }
.fp-foot__kop { margin: 0; display: flex; align-items: center; gap: 0.375rem; font: inherit; font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted-foreground); white-space: nowrap; }
.fp-foot__links { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.5rem 1rem; }
@media (min-width: 768px) { .fp-foot__links { flex-direction: column; } }
.fp-foot__link { display: inline-flex; align-items: center; gap: 0.375rem; font-size: 0.875rem; font-weight: 500; color: var(--muted-foreground); text-decoration: none; white-space: nowrap; transition: color var(--motion-instant-duration) var(--motion-instant); }
a.fp-foot__link:hover { color: var(--foreground); }
.fp-foot__link--uit { cursor: default; }
.fp-foot__soon { display: inline-flex; padding: 0.125rem 0.625rem; font-size: 0.6875rem; font-weight: 500; text-transform: none; letter-spacing: 0; color: var(--muted-foreground); background: var(--muted); border-radius: 999px; }
.fp-foot__onder { display: flex; flex-direction: column; align-items: flex-start; gap: 1rem; margin: 0.75rem 0; padding: 0 1rem; font-size: 0.875rem; color: var(--muted-foreground); }
@media (min-width: 768px) { .fp-foot__onder { flex-direction: row; align-items: center; justify-content: space-between; padding: 0 2rem; } }
.fp-foot__onder-links { display: flex; flex-direction: column; gap: 0.5rem; }
@media (min-width: 640px) { .fp-foot__onder-links { flex-direction: row; align-items: center; gap: 2rem; } }
.fp-foot__copy { margin: 0; white-space: nowrap; }
.fp-foot__legal { list-style: none; margin: 0; padding: 0; display: flex; gap: 1rem; }
.fp-foot__legal-link { color: inherit; text-decoration: none; transition: color var(--motion-instant-duration) var(--motion-instant); }
.fp-foot__legal-link:hover { color: var(--foreground); }
.fp-foot__social { list-style: none; margin: 0; padding: 0; display: flex; align-items: center; gap: 1rem; }
.fp-foot__social-link { display: flex; width: 1.25rem; height: 1.25rem; color: inherit; transition: color var(--motion-instant-duration) var(--motion-instant); }
.fp-foot__social-link:hover { color: var(--foreground); }