@struq/skill 0.5.1 → 0.5.3
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 +137 -100
- package/package.json +5 -3
- package/skills/struq/SKILL.md +168 -165
- package/skills/struq/blocks/CATALOG.md +49 -46
- package/skills/struq/blocks/backgrounds/background-aurora.tsx +2 -1
- package/skills/struq/blocks/backgrounds/background-facets.tsx +5 -3
- package/skills/struq/blocks/backgrounds/background-silk.tsx +6 -2
- package/skills/struq/blocks/catalog.json +50 -5
- package/skills/struq/blocks/film.json +11 -7
- package/skills/struq/blocks/footers/footer-contact.tsx +8 -78
- package/skills/struq/blocks/headers/header-bar.tsx +144 -29
- package/skills/struq/blocks/headers/menu-curtain.tsx +36 -19
- package/skills/struq/blocks/heroes/hero-poster.tsx +36 -10
- package/skills/struq/blocks/kit/HeroCanvas.tsx +6 -2
- package/skills/struq/blocks/kit/SplitHeadline.tsx +2 -1
- package/skills/struq/blocks/kit/accent.ts +29 -3
- package/skills/struq/blocks/kit/blocks.css +530 -6
- package/skills/struq/blocks/kit/feel.ts +183 -0
- package/skills/struq/blocks/kit/gsap.ts +9 -6
- package/skills/struq/blocks/kit/middle/PlaceHours.tsx +254 -0
- package/skills/struq/blocks/kit/middle/ProcessStrip.tsx +428 -0
- package/skills/struq/blocks/kit/middle/ServicesList.tsx +496 -0
- package/skills/struq/blocks/kit/middle/entrance.ts +98 -0
- package/skills/struq/blocks/kit/middle/hours.ts +154 -0
- package/skills/struq/blocks/kit/middle/index.ts +14 -0
- package/skills/struq/blocks/kit/motion/HoverLink.tsx +33 -0
- package/skills/struq/blocks/kit/motion/ImageCurtain.tsx +79 -0
- package/skills/struq/blocks/kit/motion/MaskWipe.tsx +91 -0
- package/skills/struq/blocks/kit/motion/Pressable.tsx +58 -0
- package/skills/struq/blocks/kit/motion/RevealText.tsx +103 -0
- package/skills/struq/blocks/kit/motion/Rise.tsx +77 -0
- package/skills/struq/blocks/kit/motion/ScrubLine.tsx +108 -0
- package/skills/struq/blocks/kit/motion/index.ts +11 -0
- package/skills/struq/blocks/kit/quiet/Close.tsx +178 -52
- package/skills/struq/blocks/kit/quiet/Statement.tsx +3 -2
- package/skills/struq/blocks/kit/subject/AppScreen.tsx +388 -0
- package/skills/struq/blocks/kit/subject/Document.tsx +742 -0
- package/skills/struq/blocks/kit/subject/Mark.tsx +44 -0
- package/skills/struq/blocks/kit/subject/PatternPaper.tsx +410 -0
- package/skills/struq/blocks/kit/subject/SectionDrawing.tsx +349 -0
- package/skills/struq/blocks/kit/subject/Ticket.tsx +354 -0
- package/skills/struq/blocks/kit/subject/build.ts +66 -0
- package/skills/struq/blocks/kit/subject/draw.ts +196 -0
- package/skills/struq/blocks/kit/subject/drawings.ts +373 -0
- package/skills/struq/blocks/kit/subject/format.ts +122 -0
- package/skills/struq/blocks/kit/subject/index.ts +11 -0
- package/skills/struq/blocks/kit/subject/patterns.ts +204 -0
- package/skills/struq/blocks/preloaders/preloader-stack.tsx +75 -22
- package/skills/struq/blocks/showpieces/hero-opener.tsx +55 -7
- package/skills/struq/blocks/showpieces/image-to-fullbleed.tsx +64 -6
- package/skills/struq/blocks/showpieces/manifesto-scrub.tsx +56 -3
- package/skills/struq/blocks/showpieces/panel-wipe.tsx +93 -11
- package/skills/struq/blocks/showpieces/wordmark-finale.tsx +38 -5
- package/skills/struq/canon/canon.json +1 -1
- package/skills/struq/references/art-direction.md +51 -0
- package/skills/struq/scripts/blocks.mjs +56 -20
- package/skills/struq/scripts/canon.mjs +36 -7
- package/skills/struq/scripts/check.mjs +417 -11
- package/skills/struq/scripts/draw.mjs +49 -0
- package/skills/struq/scripts/film-check.mjs +28 -4
- package/skills/struq/scripts/gate.mjs +13 -3
- package/skills/struq/scripts/hand.mjs +222 -0
- package/skills/struq/scripts/images.mjs +12 -3
- package/skills/struq/scripts/look.mjs +1184 -0
- package/skills/struq/scripts/stamp.mjs +47 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
// components/struq/kit/middle/ProcessStrip.tsx (Struq recipe)
|
|
3
|
+
// How the work goes from the first call to done, told by sequence in layout: never numbered kickers, never icons, never
|
|
4
|
+
// durations the brief did not give. The middle scene for a renovation, a case, a bake, a build. Three variants, each
|
|
5
|
+
// extracted from a source block:
|
|
6
|
+
//
|
|
7
|
+
// - "thread": one line draws down the page as you read; the stages hang off it left and right, and as the line's tip
|
|
8
|
+
// reaches a stage a short tick draws out to it, its term rises through its mask and its sentence follows. From the
|
|
9
|
+
// draad variant of sections/approach.
|
|
10
|
+
// - "chapters": one photo holds sticky on the left (plain CSS sticky, nothing pins) while the stages pass on the right;
|
|
11
|
+
// each stage's photo rises over the last as the stage reaches the middle of the screen, the current stage forward,
|
|
12
|
+
// the others stepped back, a hairline beside the frame filling as you read. From sections/sticky-chapters.
|
|
13
|
+
// - "rows": the stages as hairline rows, term and one sentence, an upright photo beside them. From kit/quiet/Rows,
|
|
14
|
+
// unchanged in layout.
|
|
15
|
+
//
|
|
16
|
+
// <ProcessStrip variant="thread" heading="From the first call to the last tile." steps={[
|
|
17
|
+
// { term: "Visit", text: "We come by, measure up and listen to what the room has to do." },
|
|
18
|
+
// { term: "Drawing", text: "A plan and a fixed quote, so nothing changes once we start." },
|
|
19
|
+
// { term: "Build", text: "One team from stripping out to the last silicone line." },
|
|
20
|
+
// ]} />
|
|
21
|
+
//
|
|
22
|
+
// Props
|
|
23
|
+
// - steps: { term, text, image? }[], three to six, in order. term: one or two words. text: one sentence the agent may
|
|
24
|
+
// write (what the step is). image: a StruqImage, used by "chapters".
|
|
25
|
+
// - variant: "thread" (default), "chapters" or "rows".
|
|
26
|
+
// - heading: optional, at h2. Without it the stages open the section.
|
|
27
|
+
// - label: optional, small, sentence case, beside the heading's first baseline in the margin columns (never above it).
|
|
28
|
+
// - image: optional upright photo for "rows" (beside the rows). "thread" sets no photo.
|
|
29
|
+
// - tone: "ground" (default), "inverse" or "field" (scene-inverse, scene-field in blocks.css). className: on the root.
|
|
30
|
+
//
|
|
31
|
+
// Empty slots and fallbacks (code, not advice): "chapters" needs a photo on every step and at least three steps,
|
|
32
|
+
// otherwise it renders "rows" (with `image`, or else the first step photo given, beside them; with none the rows take
|
|
33
|
+
// the full width). No heading: the stages open the scene. No label: nothing beside the heading. There is no slot for a
|
|
34
|
+
// duration, a date or a count; the order carries the time. resolveProcessVariant() returns the variant that renders.
|
|
35
|
+
//
|
|
36
|
+
// Mounting: a page section of its own. The root carries data-scene="process", data-variant (the rendered one),
|
|
37
|
+
// data-energy="1" and data-middle. No pin: chapters holds its photo with CSS sticky.
|
|
38
|
+
//
|
|
39
|
+
// Motion (kit/feel.ts, data-temperament on <body>): the heading's lines rise first (one timeline, one trigger:
|
|
40
|
+
// ./entrance). thread: the line is a ScrubLine drawn with the scroll, its tip at 62% of the screen, linear with the
|
|
41
|
+
// feel's scrub.flow; each stage fires once as the tip reaches it, its tick drawing over duration.swap, its term rising
|
|
42
|
+
// over duration.line, its sentence rising a block's travel after it. That drawing is the scene's one loud move. Crisp
|
|
43
|
+
// stages snap in short, slow stages settle long before the next. chapters: each photo's rise is scrubbed over its
|
|
44
|
+
// stage's stretch with scrub.flow smoothing, settling from a slight push-in. rows: the rows rise one after another,
|
|
45
|
+
// then the photo opens from its foot. Reduced motion: the composed scene, chapters cutting between photos. Without
|
|
46
|
+
// scripts: thread is a plain ruled list, chapters carries each photo inline, rows is composed.
|
|
47
|
+
//
|
|
48
|
+
// Phones: thread becomes one rule on the left with the stages on its right; chapters shows each photo inline above its
|
|
49
|
+
// stage; rows puts the sentence under its term and the photo after the rows.
|
|
50
|
+
import Image from "next/image";
|
|
51
|
+
import { useRef } from "react";
|
|
52
|
+
import { feel, prefersReduced } from "../feel";
|
|
53
|
+
import { gsap, ScrollTrigger, SplitText, useGSAP } from "../gsap";
|
|
54
|
+
import type { StruqImage } from "../images";
|
|
55
|
+
import { riseFrom, riseTo } from "../motion/Rise";
|
|
56
|
+
import { ScrubLine } from "../motion/ScrubLine";
|
|
57
|
+
import { LABEL, SHEET_GRID } from "../quiet/sheet";
|
|
58
|
+
import { entrance } from "./entrance";
|
|
59
|
+
import { TONE_CLASS, type SceneTone } from "./ServicesList";
|
|
60
|
+
|
|
61
|
+
export type Step = { term: string; text: string; image?: StruqImage };
|
|
62
|
+
export type ProcessVariant = "thread" | "chapters" | "rows";
|
|
63
|
+
|
|
64
|
+
type Props = {
|
|
65
|
+
steps: Step[];
|
|
66
|
+
variant?: ProcessVariant;
|
|
67
|
+
heading?: string;
|
|
68
|
+
label?: string;
|
|
69
|
+
image?: StruqImage;
|
|
70
|
+
tone?: SceneTone;
|
|
71
|
+
className?: string;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** The variant that renders: chapters needs at least three steps and a photo on every one. */
|
|
75
|
+
export function resolveProcessVariant(variant: ProcessVariant, steps: readonly Step[]): ProcessVariant {
|
|
76
|
+
if (variant === "chapters" && (steps.length < 3 || steps.some((s) => !s.image))) return "rows";
|
|
77
|
+
return variant;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** thread: where the line's tip rides, as a share of the screen from the top. */
|
|
81
|
+
const TIP = 62;
|
|
82
|
+
|
|
83
|
+
function Head({ heading, label, className = "" }: { heading?: string; label?: string; className?: string }) {
|
|
84
|
+
if (!heading && !label) return null;
|
|
85
|
+
return (
|
|
86
|
+
<div className={`items-baseline gap-y-6 ${SHEET_GRID} ${className}`}>
|
|
87
|
+
{heading ? (
|
|
88
|
+
<h2 data-part="heading" className="col-span-12 m-0 max-w-[18ch] font-display text-h2 md:col-span-7">
|
|
89
|
+
{heading}
|
|
90
|
+
</h2>
|
|
91
|
+
) : null}
|
|
92
|
+
{label ? (
|
|
93
|
+
<p data-part="detail" className={`col-span-8 md:col-span-2 md:col-start-11 ${LABEL}`}>
|
|
94
|
+
{label}
|
|
95
|
+
</p>
|
|
96
|
+
) : null}
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// --- thread ---------------------------------------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
function Thread({ steps, heading, label, tone, className }: Required<Pick<Props, "steps" | "tone" | "className">> & Pick<Props, "heading" | "label">) {
|
|
104
|
+
const ref = useRef<HTMLElement>(null);
|
|
105
|
+
|
|
106
|
+
useGSAP(
|
|
107
|
+
() => {
|
|
108
|
+
const el = ref.current;
|
|
109
|
+
if (!el) return;
|
|
110
|
+
const stop = entrance(el);
|
|
111
|
+
const stages = Array.from(el.querySelectorAll<HTMLElement>("[data-stage]"));
|
|
112
|
+
if (prefersReduced()) {
|
|
113
|
+
for (const s of stages) s.dataset.ready = "1";
|
|
114
|
+
return stop;
|
|
115
|
+
}
|
|
116
|
+
const f = feel(el);
|
|
117
|
+
const wide = window.matchMedia("(min-width: 768px)").matches;
|
|
118
|
+
const splits: SplitText[] = [];
|
|
119
|
+
const triggers: ScrollTrigger[] = [];
|
|
120
|
+
stages.forEach((stage, i) => {
|
|
121
|
+
const tick = stage.querySelector<HTMLElement>("[data-tick]");
|
|
122
|
+
const term = stage.querySelector<HTMLElement>("[data-term]");
|
|
123
|
+
const text = stage.querySelector<HTMLElement>("[data-text]");
|
|
124
|
+
const tl = gsap.timeline({ paused: true });
|
|
125
|
+
// Stages on the left on wide screens draw their tick leftwards, out of the line.
|
|
126
|
+
const left = wide && i % 2 === 0;
|
|
127
|
+
if (tick) {
|
|
128
|
+
tl.fromTo(
|
|
129
|
+
tick,
|
|
130
|
+
{ clipPath: left ? "inset(0% 0% 0% 100%)" : "inset(0% 100% 0% 0%)" },
|
|
131
|
+
{ clipPath: "inset(0% 0% 0% 0%)", duration: f.duration.swap * 0.5, ease: f.ease.swap },
|
|
132
|
+
0,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
if (term) {
|
|
136
|
+
const split = SplitText.create(term, { type: "lines", mask: "lines", linesClass: "struq-line" });
|
|
137
|
+
splits.push(split);
|
|
138
|
+
tl.fromTo(split.lines, { yPercent: f.travel.text }, { yPercent: 0, duration: f.duration.line, ease: f.ease.enter, stagger: f.stagger.line }, f.duration.swap * 0.2);
|
|
139
|
+
}
|
|
140
|
+
if (text) tl.fromTo(text, riseFrom(f), riseTo(f), f.duration.swap * 0.2 + f.stagger.block * 2);
|
|
141
|
+
stage.dataset.ready = "1";
|
|
142
|
+
triggers.push(ScrollTrigger.create({ trigger: stage, start: `top ${TIP}%`, once: true, onEnter: () => tl.play() }));
|
|
143
|
+
});
|
|
144
|
+
return () => {
|
|
145
|
+
stop();
|
|
146
|
+
for (const t of triggers) t.kill();
|
|
147
|
+
for (const s of splits) s.revert();
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
{ scope: ref, dependencies: [steps.length] },
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<section ref={ref} data-scene="process" data-variant="thread" data-energy="1" data-middle="" className={`py-28 text-ink md:py-40 ${TONE_CLASS[tone]} ${className}`}>
|
|
155
|
+
<Head heading={heading} label={label} className="mb-[10svh] md:mb-[14svh]" />
|
|
156
|
+
<div className="relative px-sheet-margin">
|
|
157
|
+
<div className="relative">
|
|
158
|
+
<span aria-hidden className="absolute inset-y-0 left-0 w-px bg-line md:left-1/2" />
|
|
159
|
+
<ScrubLine axis="y" start={`top ${TIP}%`} end={`bottom ${TIP}%`} className="absolute inset-y-0 left-0 text-ink md:left-1/2" />
|
|
160
|
+
<ol className="relative m-0 list-none p-0">
|
|
161
|
+
{steps.map((step, i) => {
|
|
162
|
+
const left = i % 2 === 0;
|
|
163
|
+
return (
|
|
164
|
+
<li key={step.term} data-stage="" className="relative grid pb-[9svh] pl-8 last:pb-0 md:grid-cols-2 md:pb-[13svh] md:pl-0">
|
|
165
|
+
<span
|
|
166
|
+
aria-hidden
|
|
167
|
+
data-tick=""
|
|
168
|
+
className={`absolute left-0 top-[0.6em] h-px w-6 bg-ink text-h2 md:w-[3vw] ${left ? "md:left-[calc(50%-3vw)]" : "md:left-1/2"}`}
|
|
169
|
+
/>
|
|
170
|
+
<div className={left ? "md:col-start-1 md:pr-[4vw] md:text-right" : "md:col-start-2 md:pl-[4vw]"}>
|
|
171
|
+
<h3 data-term="" className="m-0 font-display text-h2">
|
|
172
|
+
{step.term}
|
|
173
|
+
</h3>
|
|
174
|
+
<p data-text="" className={`m-0 mt-5 max-w-[34ch] text-body text-ink-muted ${left ? "md:ml-auto" : ""}`}>
|
|
175
|
+
{step.text}
|
|
176
|
+
</p>
|
|
177
|
+
</div>
|
|
178
|
+
</li>
|
|
179
|
+
);
|
|
180
|
+
})}
|
|
181
|
+
</ol>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
</section>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// --- chapters -------------------------------------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
/** Each stage's stretch of scroll: from its top at this share of the screen to its top at that one. */
|
|
191
|
+
const FROM = "top 70%";
|
|
192
|
+
const TO = "top 45%";
|
|
193
|
+
|
|
194
|
+
function Chapters({ steps, heading, label, tone, className }: Required<Pick<Props, "steps" | "tone" | "className">> & Pick<Props, "heading" | "label">) {
|
|
195
|
+
const ref = useRef<HTMLElement>(null);
|
|
196
|
+
|
|
197
|
+
useGSAP(
|
|
198
|
+
() => {
|
|
199
|
+
const el = ref.current;
|
|
200
|
+
if (!el) return;
|
|
201
|
+
const stop = entrance(el);
|
|
202
|
+
const fill = el.querySelector<HTMLElement>("[data-fill]");
|
|
203
|
+
const list = el.querySelector<HTMLElement>("[data-chapters]");
|
|
204
|
+
const items = Array.from(el.querySelectorAll<HTMLElement>("[data-chapter]"));
|
|
205
|
+
const layers = Array.from(el.querySelectorAll<HTMLElement>("[data-layer]"));
|
|
206
|
+
const pics = layers.map((l) => l.querySelector<HTMLElement>("[data-pic]"));
|
|
207
|
+
const imgs = layers.map((l) => l.querySelector<HTMLImageElement>("img"));
|
|
208
|
+
if (!list || items.length === 0 || layers.length === 0) return stop;
|
|
209
|
+
const f = feel(el);
|
|
210
|
+
const push = 1 + (f.travel.media - 1) * 0.4;
|
|
211
|
+
|
|
212
|
+
const mm = gsap.matchMedia();
|
|
213
|
+
mm.add({ wide: "(min-width: 1024px)", reduce: "(prefers-reduced-motion: reduce)" }, (ctx) => {
|
|
214
|
+
const { wide, reduce } = ctx.conditions as { wide: boolean; reduce: boolean };
|
|
215
|
+
if (!wide) return;
|
|
216
|
+
// A layer stays hidden until its photo is decoded, so it can only ever show the photo beneath it.
|
|
217
|
+
imgs.forEach((img, i) => {
|
|
218
|
+
if (!img || i === 0) return;
|
|
219
|
+
layers[i].style.visibility = "hidden";
|
|
220
|
+
img.loading = "eager";
|
|
221
|
+
const loaded = img.complete
|
|
222
|
+
? Promise.resolve()
|
|
223
|
+
: new Promise<void>((res) => {
|
|
224
|
+
img.addEventListener("load", () => res(), { once: true });
|
|
225
|
+
img.addEventListener("error", () => res(), { once: true });
|
|
226
|
+
});
|
|
227
|
+
loaded
|
|
228
|
+
.then(() => img.decode())
|
|
229
|
+
.catch(() => undefined)
|
|
230
|
+
.then(() => {
|
|
231
|
+
layers[i].style.visibility = "";
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
if (imgs[0]) imgs[0].loading = "eager";
|
|
235
|
+
|
|
236
|
+
const prog = layers.map((_, i) => ({ p: i === 0 ? 1 : 0 }));
|
|
237
|
+
const rise = gsap.parseEase(f.ease.swap);
|
|
238
|
+
const settle = gsap.parseEase(f.ease.enter);
|
|
239
|
+
const paint = (i: number) => {
|
|
240
|
+
const p = prog[i].p;
|
|
241
|
+
const shown = reduce ? (p >= 0.5 ? 1 : 0) : rise(p);
|
|
242
|
+
layers[i].style.clipPath = `inset(${((1 - shown) * 100).toFixed(2)}% 0% 0% 0%)`;
|
|
243
|
+
const pic = pics[i];
|
|
244
|
+
if (pic && !reduce && i > 0) {
|
|
245
|
+
const k = 1 + (push - 1) * (1 - settle(p));
|
|
246
|
+
pic.style.transform = `translate3d(0, ${((1 - settle(p)) * 4).toFixed(2)}%, 0) scale(${k.toFixed(4)})`;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
let active = -1;
|
|
250
|
+
const mark = () => {
|
|
251
|
+
let a = 0;
|
|
252
|
+
for (let i = 1; i < prog.length; i += 1) if (prog[i].p >= 0.5) a = i;
|
|
253
|
+
if (a === active) return;
|
|
254
|
+
active = a;
|
|
255
|
+
items.forEach((c, j) => {
|
|
256
|
+
if (j === a) delete c.dataset.back;
|
|
257
|
+
else c.dataset.back = "";
|
|
258
|
+
});
|
|
259
|
+
};
|
|
260
|
+
layers.forEach((l, i) => {
|
|
261
|
+
l.style.zIndex = String(i + 1);
|
|
262
|
+
paint(i);
|
|
263
|
+
});
|
|
264
|
+
mark();
|
|
265
|
+
|
|
266
|
+
const tweens = items.slice(1).map((item, k) => {
|
|
267
|
+
const i = k + 1;
|
|
268
|
+
return gsap.to(prog[i], {
|
|
269
|
+
p: 1,
|
|
270
|
+
ease: f.ease.scrub,
|
|
271
|
+
onUpdate: () => {
|
|
272
|
+
paint(i);
|
|
273
|
+
mark();
|
|
274
|
+
},
|
|
275
|
+
scrollTrigger: { trigger: item, start: FROM, end: TO, scrub: reduce ? true : f.scrub.flow },
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
const line = fill
|
|
279
|
+
? gsap.fromTo(
|
|
280
|
+
fill,
|
|
281
|
+
{ scaleY: 0 },
|
|
282
|
+
{ scaleY: 1, ease: f.ease.scrub, scrollTrigger: { trigger: list, start: "top 52%", end: "bottom 52%", scrub: reduce ? true : f.scrub.flow } },
|
|
283
|
+
)
|
|
284
|
+
: null;
|
|
285
|
+
|
|
286
|
+
return () => {
|
|
287
|
+
for (const t of tweens) {
|
|
288
|
+
t.scrollTrigger?.kill();
|
|
289
|
+
t.kill();
|
|
290
|
+
}
|
|
291
|
+
line?.scrollTrigger?.kill();
|
|
292
|
+
line?.kill();
|
|
293
|
+
items.forEach((c) => delete c.dataset.back);
|
|
294
|
+
for (const l of layers) {
|
|
295
|
+
l.style.clipPath = "";
|
|
296
|
+
l.style.zIndex = "";
|
|
297
|
+
l.style.visibility = "";
|
|
298
|
+
}
|
|
299
|
+
for (const p of pics) if (p) p.style.transform = "";
|
|
300
|
+
if (fill) fill.style.transform = "";
|
|
301
|
+
};
|
|
302
|
+
});
|
|
303
|
+
return () => {
|
|
304
|
+
stop();
|
|
305
|
+
mm.revert();
|
|
306
|
+
};
|
|
307
|
+
},
|
|
308
|
+
{ scope: ref, dependencies: [steps.length] },
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
// With scripts on desktop the sticky frame stands in the left column and the stages lose their inline photos;
|
|
312
|
+
// without scripts (or on phones) every stage keeps its own. The pre-paint class is `js` in a client project.
|
|
313
|
+
const withFrame = "hidden lg:[.js_&]:block lg:[.sq-js_&]:block";
|
|
314
|
+
const inlineOnly = "lg:[.js_&]:hidden lg:[.sq-js_&]:hidden";
|
|
315
|
+
|
|
316
|
+
return (
|
|
317
|
+
<section ref={ref} data-scene="process" data-variant="chapters" data-energy="1" data-middle="" className={`py-28 text-ink md:py-40 ${TONE_CLASS[tone]} ${className}`}>
|
|
318
|
+
<Head heading={heading} label={label} className="mb-12 lg:mb-[4svh]" />
|
|
319
|
+
<div className="px-sheet-margin lg:grid lg:grid-cols-[minmax(0,1.08fr)_minmax(0,1fr)] lg:gap-x-[6vw]">
|
|
320
|
+
<div className={withFrame}>
|
|
321
|
+
<div className="sticky top-[10svh] flex h-[80svh] gap-[1.4vw]">
|
|
322
|
+
<div className="relative h-full flex-1 overflow-hidden">
|
|
323
|
+
{steps.map((step, i) =>
|
|
324
|
+
step.image ? (
|
|
325
|
+
<div
|
|
326
|
+
key={step.term}
|
|
327
|
+
data-layer=""
|
|
328
|
+
className="absolute inset-0 overflow-hidden"
|
|
329
|
+
style={{ clipPath: i === 0 ? "inset(0% 0% 0% 0%)" : "inset(100% 0% 0% 0%)" }}
|
|
330
|
+
>
|
|
331
|
+
<div data-pic="" className="absolute inset-0">
|
|
332
|
+
<Image src={step.image.src} alt={step.image.alt} fill loading="lazy" sizes="(min-width: 1024px) 48vw, 1px" unoptimized={step.image.unoptimized} className="object-cover" />
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
) : null,
|
|
336
|
+
)}
|
|
337
|
+
</div>
|
|
338
|
+
{/* How far through the stages: a hairline that fills, never a number. */}
|
|
339
|
+
<div aria-hidden className="relative h-full w-px bg-line">
|
|
340
|
+
<div data-fill="" className="absolute inset-0 origin-top bg-ink" style={{ transform: "scaleY(0)" }} />
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
|
|
345
|
+
<ol data-chapters="" className="m-0 grid list-none gap-16 p-0 lg:block">
|
|
346
|
+
{steps.map((step) => (
|
|
347
|
+
<li
|
|
348
|
+
key={step.term}
|
|
349
|
+
data-chapter=""
|
|
350
|
+
className="group/item lg:[.js_&]:flex lg:[.js_&]:min-h-[80svh] lg:[.js_&]:items-center lg:[.sq-js_&]:flex lg:[.sq-js_&]:min-h-[80svh] lg:[.sq-js_&]:items-center"
|
|
351
|
+
>
|
|
352
|
+
{step.image ? (
|
|
353
|
+
<span className={`relative mb-6 block aspect-[4/5] w-full overflow-hidden sm:aspect-[4/3] lg:mb-10 ${inlineOnly}`}>
|
|
354
|
+
<Image src={step.image.src} alt={step.image.alt} fill sizes="(min-width: 1024px) 44vw, 100vw" unoptimized={step.image.unoptimized} className="object-cover" />
|
|
355
|
+
</span>
|
|
356
|
+
) : null}
|
|
357
|
+
<div data-chapter-text="" className="max-w-[30rem]">
|
|
358
|
+
<h3 className="m-0 font-display text-h2 text-ink">{step.term}</h3>
|
|
359
|
+
<p className="m-0 mt-5 text-body text-ink-muted">{step.text}</p>
|
|
360
|
+
</div>
|
|
361
|
+
</li>
|
|
362
|
+
))}
|
|
363
|
+
</ol>
|
|
364
|
+
</div>
|
|
365
|
+
</section>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// --- rows -----------------------------------------------------------------------------------------------------------
|
|
370
|
+
|
|
371
|
+
function StepRows({ steps, heading, label, image, tone, className }: Required<Pick<Props, "steps" | "tone" | "className">> & Pick<Props, "heading" | "label" | "image">) {
|
|
372
|
+
const ref = useRef<HTMLElement>(null);
|
|
373
|
+
useGSAP(
|
|
374
|
+
() => {
|
|
375
|
+
const el = ref.current;
|
|
376
|
+
if (!el) return;
|
|
377
|
+
return entrance(el);
|
|
378
|
+
},
|
|
379
|
+
{ scope: ref, dependencies: [steps.length, Boolean(image)] },
|
|
380
|
+
);
|
|
381
|
+
const wide = !image;
|
|
382
|
+
return (
|
|
383
|
+
<section ref={ref} data-scene="process" data-variant="rows" data-energy="1" data-middle="" className={`py-28 text-ink md:py-40 ${TONE_CLASS[tone]} ${className}`}>
|
|
384
|
+
<div className={`items-start gap-y-10 md:gap-y-16 ${SHEET_GRID}`}>
|
|
385
|
+
{heading ? (
|
|
386
|
+
<h2 data-part="heading" className="col-span-12 m-0 text-h2 md:col-span-7 md:self-baseline">
|
|
387
|
+
{heading}
|
|
388
|
+
</h2>
|
|
389
|
+
) : null}
|
|
390
|
+
{label ? (
|
|
391
|
+
<p data-part="detail" className={`col-span-8 md:col-span-2 md:col-start-11 md:self-baseline ${LABEL}`}>
|
|
392
|
+
{label}
|
|
393
|
+
</p>
|
|
394
|
+
) : null}
|
|
395
|
+
<ol className={`col-span-12 m-0 grid list-none border-b border-line p-0 md:grid-cols-subgrid ${wide ? "md:col-span-12" : "md:col-span-8"}`}>
|
|
396
|
+
{steps.map((step) => (
|
|
397
|
+
<li
|
|
398
|
+
key={step.term}
|
|
399
|
+
data-part="detail"
|
|
400
|
+
className="grid gap-y-2 border-t border-line py-6 md:col-span-full md:grid-cols-subgrid md:items-baseline md:py-8"
|
|
401
|
+
>
|
|
402
|
+
<h3 className="m-0 text-h3 md:col-span-3">{step.term}</h3>
|
|
403
|
+
<p className={`m-0 max-w-[48ch] text-body text-ink-muted ${wide ? "md:col-span-6 md:col-start-5" : "md:col-span-5"}`}>{step.text}</p>
|
|
404
|
+
</li>
|
|
405
|
+
))}
|
|
406
|
+
</ol>
|
|
407
|
+
{image ? (
|
|
408
|
+
<div className="col-span-8 col-start-5 md:col-span-3 md:col-start-10">
|
|
409
|
+
<div data-part="media" data-open="bottom" className="relative aspect-[4/5] overflow-hidden">
|
|
410
|
+
<div className="absolute inset-0">
|
|
411
|
+
<Image src={image.src} alt={image.alt} fill sizes="(min-width: 768px) 25vw, 66vw" unoptimized={image.unoptimized} className="object-cover" />
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
) : null}
|
|
416
|
+
</div>
|
|
417
|
+
</section>
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export function ProcessStrip({ steps, variant = "thread", heading, label, image, tone = "ground", className = "" }: Props) {
|
|
422
|
+
const shown = resolveProcessVariant(variant, steps);
|
|
423
|
+
if (shown === "thread") return <Thread steps={steps} heading={heading} label={label} tone={tone} className={className} />;
|
|
424
|
+
if (shown === "chapters") return <Chapters steps={steps} heading={heading} label={label} tone={tone} className={className} />;
|
|
425
|
+
// rows, asked for or fallen back to: the given photo, else the first step photo, else none (full width).
|
|
426
|
+
const photo = image ?? steps.find((s) => s.image)?.image;
|
|
427
|
+
return <StepRows steps={steps} heading={heading} label={label} image={photo} tone={tone} className={className} />;
|
|
428
|
+
}
|