@struq/skill 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -36
- package/package.json +3 -2
- package/skills/struq/SKILL.md +139 -307
- package/skills/struq/blocks/CATALOG.md +426 -0
- package/skills/struq/blocks/backgrounds/background-aurora.tsx +818 -0
- package/skills/struq/blocks/backgrounds/background-facets.tsx +1283 -0
- package/skills/struq/blocks/backgrounds/background-silk.tsx +906 -0
- package/skills/struq/blocks/catalog.json +1302 -0
- package/skills/struq/blocks/footers/footer-contact.tsx +789 -0
- package/skills/struq/blocks/footers/footer-reveal.tsx +313 -0
- package/skills/struq/blocks/headers/header-bar.tsx +652 -0
- package/skills/struq/blocks/headers/menu-curtain.tsx +790 -0
- package/skills/struq/blocks/heroes/hero-framed.tsx +627 -0
- package/skills/struq/blocks/heroes/hero-index.tsx +855 -0
- package/skills/struq/blocks/heroes/hero-poster.tsx +1086 -0
- package/skills/struq/blocks/heroes/hero-split.tsx +685 -0
- package/skills/struq/blocks/heroes/hero-still.tsx +862 -0
- package/skills/struq/{recipes/components/struq → blocks/kit}/Cursor.tsx +1 -1
- package/skills/struq/{recipes/components/struq → blocks/kit}/HeroCanvas.tsx +2 -2
- package/skills/struq/{recipes/components/struq → blocks/kit}/HorizontalGallery.tsx +2 -2
- package/skills/struq/{recipes/components/struq → blocks/kit}/MagneticButton.tsx +1 -1
- package/skills/struq/{recipes/components/struq → blocks/kit}/MediaFrame.tsx +2 -2
- package/skills/struq/{recipes/components/struq → blocks/kit}/PinnedZoom.tsx +14 -4
- package/skills/struq/{recipes/components/struq → blocks/kit}/Reveal.tsx +1 -1
- package/skills/struq/{recipes/components/struq → blocks/kit}/SmoothScroll.tsx +1 -1
- package/skills/struq/{recipes/components/struq → blocks/kit}/SplitHeadline.tsx +1 -1
- package/skills/struq/blocks/kit/blocks.css +2102 -0
- package/skills/struq/blocks/kit/images.ts +17 -0
- package/skills/struq/blocks/kit/photo-read.ts +102 -0
- package/skills/struq/blocks/page-transitions/page-transition-gate.tsx +706 -0
- package/skills/struq/blocks/page-transitions/page-transition-slices.tsx +469 -0
- package/skills/struq/blocks/page-transitions/page-transition-stack.tsx +583 -0
- package/skills/struq/blocks/preloaders/preloader-gate.tsx +358 -0
- package/skills/struq/blocks/preloaders/preloader-slices-uncovered.ts +50 -0
- package/skills/struq/blocks/preloaders/preloader-slices.tsx +289 -0
- package/skills/struq/blocks/preloaders/preloader-stack.tsx +367 -0
- package/skills/struq/blocks/preloaders/preloader-wordmark.tsx +568 -0
- package/skills/struq/blocks/sections/about-studio.tsx +436 -0
- package/skills/struq/blocks/sections/approach.tsx +661 -0
- package/skills/struq/blocks/sections/contact-form.tsx +664 -0
- package/skills/struq/blocks/sections/cta-closing.tsx +763 -0
- package/skills/struq/blocks/sections/fanned-cards.tsx +547 -0
- package/skills/struq/blocks/sections/film-strip.tsx +385 -0
- package/skills/struq/blocks/sections/grid-assembly.tsx +616 -0
- package/skills/struq/blocks/sections/hover-list.tsx +467 -0
- package/skills/struq/blocks/sections/image-trail.tsx +432 -0
- package/skills/struq/blocks/sections/manifesto.tsx +691 -0
- package/skills/struq/blocks/sections/panel-accordion.tsx +380 -0
- package/skills/struq/blocks/sections/reveals.tsx +416 -0
- package/skills/struq/blocks/sections/split-pair.tsx +670 -0
- package/skills/struq/blocks/sections/sticky-chapters.tsx +256 -0
- package/skills/struq/blocks/sections/velocity-type.tsx +269 -0
- package/skills/struq/{cinematic/showpieces/toonstuk-fill-rows.tsx → blocks/showpieces/fill-rows.tsx} +1 -1
- package/skills/struq/{cinematic/showpieces/toonstuk-hero-opener.tsx → blocks/showpieces/hero-opener.tsx} +5 -5
- package/skills/struq/{cinematic/showpieces/toonstuk-image-to-fullbleed.tsx → blocks/showpieces/image-to-fullbleed.tsx} +16 -5
- package/skills/struq/blocks/showpieces/manifesto-scrub.tsx +48 -0
- package/skills/struq/{cinematic/showpieces/toonstuk-panel-wipe.tsx → blocks/showpieces/panel-wipe.tsx} +2 -2
- package/skills/struq/{cinematic/showpieces/toonstuk-pixel-resolve.tsx → blocks/showpieces/pixel-resolve.tsx} +1 -1
- package/skills/struq/{cinematic/showpieces/toonstuk-project-stack.tsx → blocks/showpieces/project-stack.tsx} +2 -2
- package/skills/struq/blocks/showpieces/scrub-connector.tsx +307 -0
- package/skills/struq/{cinematic/showpieces/toonstuk-wordmark-finale.tsx → blocks/showpieces/wordmark-finale.tsx} +1 -1
- package/skills/struq/blocks/transitions/zoom-through.tsx +613 -0
- package/skills/struq/canon/canon.json +627 -0
- package/skills/struq/cinematic/README.md +7 -18
- package/skills/struq/cinematic/examples/code/nextjs/app/components/LenisRoot.tsx +1 -1
- package/skills/struq/cinematic/examples/code/shared/styles.css +1 -1
- package/skills/struq/cinematic/examples/code/vite/src/LenisRoot.tsx +1 -1
- package/skills/struq/cinematic/references/01-core-mental-model.md +1 -1
- package/skills/struq/cinematic/references/02-vocabulary.md +1 -1
- package/skills/struq/cinematic/references/07-stack-integration.md +2 -2
- package/skills/struq/recipes/app/fonts.ts +12 -22
- package/skills/struq/recipes/app/globals.css +34 -177
- package/skills/struq/recipes/app/layout.tsx +13 -11
- package/skills/struq/references/craft.md +77 -0
- package/skills/struq/references/iconography.md +28 -230
- package/skills/struq/references/stack-targets.md +23 -74
- package/skills/struq/references/tells.md +52 -0
- package/skills/struq/references/triage.md +6 -7
- package/skills/struq/scripts/blocks.mjs +156 -0
- package/skills/struq/scripts/canon.mjs +106 -281
- package/skills/struq/scripts/check.mjs +92 -126
- package/skills/struq/scripts/images.mjs +2 -2
- package/skills/struq/scripts/log.mjs +6 -23
- package/skills/struq/scripts/tokens.mjs +222 -0
- package/skills/struq/cinematic/showpieces/README.md +0 -18
- package/skills/struq/cinematic/showpieces/toonstuk-manifesto-scrub.tsx +0 -81
- package/skills/struq/cinematic/showpieces/toonstuk-scrub-connector.tsx +0 -78
- package/skills/struq/references/anti-patterns.md +0 -330
- package/skills/struq/references/baseline-prompt.md +0 -122
- package/skills/struq/references/brand.md +0 -143
- package/skills/struq/references/brief-router.md +0 -218
- package/skills/struq/references/composition.md +0 -190
- package/skills/struq/references/depth-parallax.md +0 -235
- package/skills/struq/references/direction.md +0 -188
- package/skills/struq/references/feel-layer.md +0 -107
- package/skills/struq/references/gsap-api.md +0 -293
- package/skills/struq/references/hero-scenes.md +0 -107
- package/skills/struq/references/imagery.md +0 -125
- package/skills/struq/references/loaders.md +0 -287
- package/skills/struq/references/micro-interactions.md +0 -270
- package/skills/struq/references/mobile-composition.md +0 -70
- package/skills/struq/references/nav-header.md +0 -296
- package/skills/struq/references/niche-packets.md +0 -338
- package/skills/struq/references/page-architecture.md +0 -134
- package/skills/struq/references/page-entrances.md +0 -198
- package/skills/struq/references/performance.md +0 -235
- package/skills/struq/references/pinning-scrub.md +0 -340
- package/skills/struq/references/pitfalls.md +0 -290
- package/skills/struq/references/route-transitions.md +0 -281
- package/skills/struq/references/scaling.md +0 -151
- package/skills/struq/references/scroll-reveals.md +0 -279
- package/skills/struq/references/sections.md +0 -141
- package/skills/struq/references/seo.md +0 -219
- package/skills/struq/references/setup.md +0 -397
- package/skills/struq/references/showcase.md +0 -87
- package/skills/struq/references/struq-imagery.md +0 -107
- package/skills/struq/references/svg-motion.md +0 -390
- package/skills/struq/references/transitions.md +0 -160
- package/skills/struq/references/typography.md +0 -720
- package/skills/struq/references/vibe-presets.md +0 -287
- package/skills/struq/references/webgl-r3f.md +0 -274
- package/skills/struq/references/webgl-scenes.md +0 -593
- package/skills/struq/scripts/font-names.mjs +0 -40
- package/skills/struq/scripts/recipes.mjs +0 -135
- /package/skills/struq/{recipes/lib → blocks/kit}/gsap.ts +0 -0
- /package/skills/struq/{recipes/components/struq → blocks/kit}/scenes/contours.ts +0 -0
- /package/skills/struq/{recipes/components/struq → blocks/kit}/scenes/dots.ts +0 -0
- /package/skills/struq/{recipes/components/struq → blocks/kit}/scenes/ripple.ts +0 -0
- /package/skills/struq/{recipes/components/struq → blocks/kit}/scenes/types.ts +0 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
// footer-reveal (Struq footer recipe)
|
|
3
|
+
// The footer is already there, under the page. As the reader reaches the end, the last section lifts away like a
|
|
4
|
+
// sheet and uncovers it, scrubbed to the scroll; scrolling back lays the page down again. The footer itself is calm
|
|
5
|
+
// and complete: the name large at a light weight, one closing line, the mail address and the phone number as real
|
|
6
|
+
// links, the street address, a few plain links and a small foot line. Three uncoverings:
|
|
7
|
+
//
|
|
8
|
+
// - lift: the last section rises and its bottom edge carries a hairline and a short soft shadow while it leaves; the
|
|
9
|
+
// footer stands still beneath it. The classic sticky reveal.
|
|
10
|
+
// - fold: the last section tilts back from its bottom edge a few degrees as it lifts, like a page turned up (the hinge
|
|
11
|
+
// is a screen above the edge, so the tilt is the same whatever the section's height), and the footer brightens as
|
|
12
|
+
// it is uncovered.
|
|
13
|
+
// - iris: the footer is uncovered through an opening that grows from a thin horizontal slit at the bottom of the
|
|
14
|
+
// screen to the full footer, its clip written from plain numbers, and the name rises inside it.
|
|
15
|
+
//
|
|
16
|
+
// <FooterReveal variant="lift" name="Noord" line="Kom langs, de tekentafel staat klaar." email="hallo@noord.studio"
|
|
17
|
+
// phone="+31 10 412 77 30" address={["Maashaven Zuidzijde 14", "3081 AE Rotterdam"]}
|
|
18
|
+
// links={[{ label: "Werk", href: "/werk" }, { label: "Instagram", href: "https://instagram.com/noord" }]}>
|
|
19
|
+
// <section>the page's last section</section>
|
|
20
|
+
// </FooterReveal>
|
|
21
|
+
//
|
|
22
|
+
// How it holds: the last section (children) and the footer share one wrapper; the section sits above, the footer is
|
|
23
|
+
// `position: sticky` to the bottom of the screen beneath it. So the document is exactly as tall as the two of them
|
|
24
|
+
// (no margin trick, no fixed layer, no spacer) and the hand-over has nothing to switch: when the section has gone, the
|
|
25
|
+
// footer simply is where it always was. A footer taller than the screen sticks by its top instead (its sticky bottom
|
|
26
|
+
// is the screen height minus its own), so the lift uncovers its first screen and then the footer scrolls on.
|
|
27
|
+
// Every effect is painted from the raw scroll progress in onUpdate (Lenis already smooths the scroll), so the shadow,
|
|
28
|
+
// the tilt and the clip stay locked to the section's real edge and a fast flick never stacks states.
|
|
29
|
+
//
|
|
30
|
+
// Without scripts and under reduced motion nothing is sticky and nothing is hidden: the footer follows the page.
|
|
31
|
+
// Keyboard: a link in the footer that takes focus while the section still covers it scrolls the footer clear.
|
|
32
|
+
// The last section should be at least a screen tall; a shorter one still opens no gap, the footer just follows sooner.
|
|
33
|
+
import { useId, useRef, type ReactNode } from "react";
|
|
34
|
+
import { gsap, ScrollTrigger, useGSAP } from "../kit/gsap";
|
|
35
|
+
import { getLenis } from "../kit/SmoothScroll";
|
|
36
|
+
|
|
37
|
+
export const FOOTER_REVEAL_VARIANTS = [
|
|
38
|
+
{ id: "lift", title: "Optillen" },
|
|
39
|
+
{ id: "fold", title: "Omslaan" },
|
|
40
|
+
{ id: "iris", title: "Opening" },
|
|
41
|
+
] as const;
|
|
42
|
+
|
|
43
|
+
export type FooterRevealVariant = (typeof FOOTER_REVEAL_VARIANTS)[number]["id"];
|
|
44
|
+
|
|
45
|
+
type FooterLink = { label: string; href: string };
|
|
46
|
+
|
|
47
|
+
type Props = {
|
|
48
|
+
variant?: FooterRevealVariant;
|
|
49
|
+
/** The page's last section: the sheet that lifts away. */
|
|
50
|
+
children: ReactNode;
|
|
51
|
+
name: string;
|
|
52
|
+
/** One short closing line. */
|
|
53
|
+
line: string;
|
|
54
|
+
email: string;
|
|
55
|
+
/** As it is read, "+31 10 412 77 30"; the tel: link is made from its digits. */
|
|
56
|
+
phone?: string;
|
|
57
|
+
/** The street address, one entry per line. */
|
|
58
|
+
address: string[];
|
|
59
|
+
/** Three or four plain links. */
|
|
60
|
+
links: FooterLink[];
|
|
61
|
+
year?: number;
|
|
62
|
+
className?: string;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** The fold's largest tilt, in degrees, and its perspective as a share of the screen height. */
|
|
66
|
+
const TILT = 3.5;
|
|
67
|
+
const DEPTH = 2.4;
|
|
68
|
+
/** The iris: the slit's height in px, and the share of the width shut at each side while it is a slit. */
|
|
69
|
+
const SLIT = 3;
|
|
70
|
+
const SIDE_WIDE = 0.3;
|
|
71
|
+
const SIDE_PHONE = 0.14;
|
|
72
|
+
/** The name's rise inside the opening, as a share of its own height. */
|
|
73
|
+
const NAME_RISE = 0.55;
|
|
74
|
+
|
|
75
|
+
const clamp01 = (v: number) => Math.min(1, Math.max(0, v));
|
|
76
|
+
/** Where p sits inside the window [from, to], as 0 to 1. */
|
|
77
|
+
const within = (p: number, from: number, to: number) => clamp01((p - from) / (to - from));
|
|
78
|
+
const out2 = gsap.parseEase("power2.out");
|
|
79
|
+
const out3 = gsap.parseEase("power3.out");
|
|
80
|
+
const inOut2 = gsap.parseEase("power2.inOut");
|
|
81
|
+
|
|
82
|
+
/** How deep the shade over the footer starts: lighter on a light ground, deeper on a dark one, or it does not show. */
|
|
83
|
+
function dimFor(el: HTMLElement) {
|
|
84
|
+
const m = getComputedStyle(el).backgroundColor.match(/\d+(\.\d+)?/g);
|
|
85
|
+
if (!m || m.length < 3) return 0.35;
|
|
86
|
+
const [r, g, b] = m.map(Number);
|
|
87
|
+
return r * 0.299 + g * 0.587 + b * 0.114 < 128 ? 0.6 : 0.32;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function FooterReveal({
|
|
91
|
+
variant = "lift",
|
|
92
|
+
children,
|
|
93
|
+
name,
|
|
94
|
+
line,
|
|
95
|
+
email,
|
|
96
|
+
phone,
|
|
97
|
+
address,
|
|
98
|
+
links,
|
|
99
|
+
year = new Date().getFullYear(),
|
|
100
|
+
className = "",
|
|
101
|
+
}: Props) {
|
|
102
|
+
const root = useRef<HTMLDivElement>(null);
|
|
103
|
+
const titleId = useId();
|
|
104
|
+
const iris = variant === "iris";
|
|
105
|
+
|
|
106
|
+
useGSAP(
|
|
107
|
+
() => {
|
|
108
|
+
const el = root.current;
|
|
109
|
+
const sheet = el?.querySelector<HTMLElement>("[data-fr-sheet]");
|
|
110
|
+
const foot = el?.querySelector<HTMLElement>("[data-fr-foot]");
|
|
111
|
+
if (!el || !sheet || !foot) return;
|
|
112
|
+
const edge = el.querySelector<HTMLElement>("[data-fr-edge]");
|
|
113
|
+
const shade = el.querySelector<HTMLElement>("[data-fr-shade]");
|
|
114
|
+
const mark = el.querySelector<HTMLElement>("[data-fr-name]");
|
|
115
|
+
|
|
116
|
+
const mm = gsap.matchMedia();
|
|
117
|
+
mm.add("(prefers-reduced-motion: no-preference)", () => {
|
|
118
|
+
// The measures every paint reads, taken again on every refresh: the screen, the footer, the part of the
|
|
119
|
+
// footer the lift uncovers (all of it, or its first screen when it is taller), and the sheet.
|
|
120
|
+
let vw = window.innerWidth;
|
|
121
|
+
let vh = window.innerHeight;
|
|
122
|
+
let footH = foot.offsetHeight;
|
|
123
|
+
let reveal = Math.min(footH, vh);
|
|
124
|
+
let sheetH = sheet.offsetHeight;
|
|
125
|
+
let nameH = mark?.offsetHeight ?? 0;
|
|
126
|
+
let dim = dimFor(foot);
|
|
127
|
+
let p = 0;
|
|
128
|
+
|
|
129
|
+
const measure = () => {
|
|
130
|
+
vw = window.innerWidth;
|
|
131
|
+
vh = window.innerHeight;
|
|
132
|
+
footH = foot.offsetHeight;
|
|
133
|
+
reveal = Math.min(footH, vh);
|
|
134
|
+
sheetH = sheet.offsetHeight;
|
|
135
|
+
nameH = mark?.offsetHeight ?? 0;
|
|
136
|
+
dim = dimFor(foot);
|
|
137
|
+
// A footer that fits sticks by its bottom; a taller one by its top, so its first screen is what lifts free.
|
|
138
|
+
el.style.setProperty("--fr-bottom", `${Math.min(0, vh - footH)}px`);
|
|
139
|
+
if (variant === "fold") sheet.style.transformOrigin = `50% ${Math.max(0, sheetH - vh)}px`;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const paint = () => {
|
|
143
|
+
if (edge) edge.style.opacity = (within(p, 0, 0.05) * (1 - within(p, 0.8, 1))).toFixed(3);
|
|
144
|
+
|
|
145
|
+
if (variant === "fold") {
|
|
146
|
+
// The free bottom edge lifts toward the reader (a positive tilt), never away into the screen. The tilt rises
|
|
147
|
+
// early and holds; where the section's edge still shows at rest (a footer shorter than
|
|
148
|
+
// the screen), it lays flat again before the end, so the hand-over never shows a wedge.
|
|
149
|
+
const flatten = clamp01((vh - reveal) / (0.2 * vh));
|
|
150
|
+
const a = TILT * out2(within(p, 0, 0.45)) * (1 - flatten * inOut2(within(p, 0.55, 1)));
|
|
151
|
+
sheet.style.transform = a > 0.005 ? `perspective(${Math.round(vh * DEPTH)}px) rotateX(${a.toFixed(3)}deg)` : "";
|
|
152
|
+
if (shade) shade.style.opacity = (dim * (1 - inOut2(p))).toFixed(3);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (iris) {
|
|
156
|
+
if (p >= 1) {
|
|
157
|
+
foot.style.clipPath = "none";
|
|
158
|
+
} else {
|
|
159
|
+
// In the footer's own pixels: the opening's bottom is the bottom of the screen, its top climbs from a
|
|
160
|
+
// slit locked to the section's edge (no band of ground between them), and its sides open within the first fifth
|
|
161
|
+
// of the move, so no line of text is ever cut at the opening's side.
|
|
162
|
+
const height = SLIT + (reveal - SLIT) * p;
|
|
163
|
+
const top = Math.max(0, reveal - height);
|
|
164
|
+
const side = vw * (vw < 768 ? SIDE_PHONE : SIDE_WIDE) * (1 - out3(within(p, 0, 0.2)));
|
|
165
|
+
foot.style.clipPath = `inset(${top.toFixed(1)}px ${side.toFixed(1)}px 0px ${side.toFixed(1)}px)`;
|
|
166
|
+
}
|
|
167
|
+
if (mark) {
|
|
168
|
+
// The name rises once the opening has reached it, so the rise is seen.
|
|
169
|
+
const e = out2(within(p, 0.6, 1));
|
|
170
|
+
mark.style.transform = `translate3d(0, ${Math.round((1 - e) * nameH * NAME_RISE)}px, 0)`;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
measure();
|
|
176
|
+
el.dataset.live = variant;
|
|
177
|
+
if (variant === "fold") sheet.style.willChange = "transform";
|
|
178
|
+
|
|
179
|
+
const st = ScrollTrigger.create({
|
|
180
|
+
trigger: el,
|
|
181
|
+
// The sheet's bottom edge reaching the bottom of the screen, measured on the untransformed wrapper.
|
|
182
|
+
start: () => `top+=${sheet.offsetHeight} bottom`,
|
|
183
|
+
end: () => `+=${Math.min(foot.offsetHeight, window.innerHeight)}`,
|
|
184
|
+
onRefresh: (self) => {
|
|
185
|
+
measure();
|
|
186
|
+
p = self.progress;
|
|
187
|
+
paint();
|
|
188
|
+
},
|
|
189
|
+
onUpdate: (self) => {
|
|
190
|
+
p = self.progress;
|
|
191
|
+
paint();
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
p = st.progress;
|
|
195
|
+
paint();
|
|
196
|
+
|
|
197
|
+
// The footer or the sheet changing height (a face landing, a line wrapping) moves the stretch: measure again.
|
|
198
|
+
let frame = 0;
|
|
199
|
+
const heights = [footH, sheetH];
|
|
200
|
+
const ro = new ResizeObserver(() => {
|
|
201
|
+
if (foot.offsetHeight === heights[0] && sheet.offsetHeight === heights[1]) return;
|
|
202
|
+
heights[0] = foot.offsetHeight;
|
|
203
|
+
heights[1] = sheet.offsetHeight;
|
|
204
|
+
cancelAnimationFrame(frame);
|
|
205
|
+
frame = requestAnimationFrame(() => ScrollTrigger.refresh());
|
|
206
|
+
});
|
|
207
|
+
ro.observe(foot);
|
|
208
|
+
ro.observe(sheet);
|
|
209
|
+
|
|
210
|
+
// A link that takes focus while the sheet still covers the footer: scroll until the footer is clear (and,
|
|
211
|
+
// in a tall footer, until the link itself is on screen).
|
|
212
|
+
const onFocus = (e: FocusEvent) => {
|
|
213
|
+
if (p >= 0.999) return;
|
|
214
|
+
const target = e.target as HTMLElement;
|
|
215
|
+
const below = target.getBoundingClientRect().bottom - foot.getBoundingClientRect().top;
|
|
216
|
+
const over = vh - reveal + below - (vh - 24);
|
|
217
|
+
const y = st.end + Math.max(0, over);
|
|
218
|
+
const lenis = getLenis();
|
|
219
|
+
if (lenis) lenis.scrollTo(y, { duration: 0.7 });
|
|
220
|
+
else window.scrollTo({ top: y });
|
|
221
|
+
};
|
|
222
|
+
foot.addEventListener("focusin", onFocus);
|
|
223
|
+
|
|
224
|
+
return () => {
|
|
225
|
+
foot.removeEventListener("focusin", onFocus);
|
|
226
|
+
ro.disconnect();
|
|
227
|
+
cancelAnimationFrame(frame);
|
|
228
|
+
st.kill();
|
|
229
|
+
delete el.dataset.live;
|
|
230
|
+
el.style.removeProperty("--fr-bottom");
|
|
231
|
+
sheet.style.transform = "";
|
|
232
|
+
sheet.style.transformOrigin = "";
|
|
233
|
+
sheet.style.willChange = "";
|
|
234
|
+
foot.style.clipPath = "";
|
|
235
|
+
if (edge) edge.style.opacity = "";
|
|
236
|
+
if (shade) shade.style.opacity = "";
|
|
237
|
+
if (mark) mark.style.transform = "";
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
return () => mm.revert();
|
|
241
|
+
},
|
|
242
|
+
{ scope: root, dependencies: [variant] },
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const tel = phone ? `tel:${phone.replace(/[^\d+]/g, "")}` : null;
|
|
246
|
+
const link = "underline decoration-transparent underline-offset-[0.22em] transition-[text-decoration-color] duration-200 hover:decoration-current focus-visible:decoration-current";
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<div ref={root} data-footer-reveal={variant} className={className}>
|
|
250
|
+
<div data-fr-sheet="">
|
|
251
|
+
{children}
|
|
252
|
+
{iris ? null : <div data-fr-edge="" aria-hidden />}
|
|
253
|
+
</div>
|
|
254
|
+
<footer
|
|
255
|
+
data-fr-foot=""
|
|
256
|
+
aria-labelledby={titleId}
|
|
257
|
+
className={iris ? "scene-inverse" : "bg-surface-1 text-ink"}
|
|
258
|
+
>
|
|
259
|
+
{variant === "fold" ? <div data-fr-shade="" aria-hidden /> : null}
|
|
260
|
+
<div className="relative flex min-h-svh flex-col px-6 pb-6 pt-[max(4rem,11svh)] md:px-[3vw] md:pt-[max(4.5rem,13svh)]">
|
|
261
|
+
<div className={iris ? "flex flex-col items-center text-center" : ""}>
|
|
262
|
+
<h2
|
|
263
|
+
id={titleId}
|
|
264
|
+
data-fr-name=""
|
|
265
|
+
className="m-0 text-[min(24vw,24svh)] leading-[0.86] tracking-[-0.04em] md:text-[min(13vw,25svh)]"
|
|
266
|
+
>
|
|
267
|
+
{name}
|
|
268
|
+
</h2>
|
|
269
|
+
<p className="m-0 mt-[3svh] max-w-[30ch] font-display text-h3 text-ink-muted">{line}</p>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<div aria-hidden className="min-h-[7svh] flex-1" />
|
|
273
|
+
|
|
274
|
+
<div className="grid gap-7 border-t border-line pt-6 text-body md:grid-cols-12 md:gap-6">
|
|
275
|
+
<address className="flex flex-col not-italic md:col-span-4">
|
|
276
|
+
<a href={`mailto:${email}`} className={`text-lead text-ink ${link}`}>
|
|
277
|
+
{email}
|
|
278
|
+
</a>
|
|
279
|
+
{tel ? (
|
|
280
|
+
<a href={tel} className={`mt-1 text-ink-muted ${link}`}>
|
|
281
|
+
{phone}
|
|
282
|
+
</a>
|
|
283
|
+
) : null}
|
|
284
|
+
</address>
|
|
285
|
+
<p className="m-0 text-ink-muted md:col-span-4">
|
|
286
|
+
{address.map((part, i) => (
|
|
287
|
+
<span key={part} className="block">
|
|
288
|
+
{part}
|
|
289
|
+
{i < address.length - 1 ? <span className="sr-only">, </span> : null}
|
|
290
|
+
</span>
|
|
291
|
+
))}
|
|
292
|
+
</p>
|
|
293
|
+
<nav aria-label={`${name}, verder`} className="md:col-span-3 md:col-start-10">
|
|
294
|
+
<ul className="m-0 flex list-none flex-wrap gap-x-6 gap-y-1 p-0 md:flex-col">
|
|
295
|
+
{links.map((l) => (
|
|
296
|
+
<li key={l.href}>
|
|
297
|
+
<a href={l.href} className={`text-ink ${link}`}>
|
|
298
|
+
{l.label}
|
|
299
|
+
</a>
|
|
300
|
+
</li>
|
|
301
|
+
))}
|
|
302
|
+
</ul>
|
|
303
|
+
</nav>
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
<p className="m-0 mt-10 text-small text-ink-muted">
|
|
307
|
+
© {year} {name}
|
|
308
|
+
</p>
|
|
309
|
+
</div>
|
|
310
|
+
</footer>
|
|
311
|
+
</div>
|
|
312
|
+
);
|
|
313
|
+
}
|