@struq/skill 0.2.1 → 0.3.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 +15 -5
- package/package.json +4 -4
- package/skills/struq/SKILL.md +304 -200
- package/skills/struq/cinematic/showpieces/README.md +18 -0
- package/skills/struq/cinematic/showpieces/toonstuk-fill-rows.tsx +74 -0
- package/skills/struq/cinematic/showpieces/toonstuk-hero-opener.tsx +128 -0
- package/skills/struq/cinematic/showpieces/toonstuk-image-to-fullbleed.tsx +78 -0
- package/skills/struq/cinematic/showpieces/toonstuk-manifesto-scrub.tsx +81 -0
- package/skills/struq/cinematic/showpieces/toonstuk-panel-wipe.tsx +89 -0
- package/skills/struq/cinematic/showpieces/toonstuk-pixel-resolve.tsx +107 -0
- package/skills/struq/cinematic/showpieces/toonstuk-project-stack.tsx +86 -0
- package/skills/struq/cinematic/showpieces/toonstuk-scrub-connector.tsx +78 -0
- package/skills/struq/cinematic/showpieces/toonstuk-wordmark-finale.tsx +105 -0
- package/skills/struq/recipes/app/fonts.ts +22 -0
- package/skills/struq/recipes/app/globals.css +185 -0
- package/skills/struq/recipes/app/layout.tsx +28 -0
- package/skills/struq/recipes/components/struq/Cursor.tsx +117 -0
- package/skills/struq/recipes/components/struq/HeroCanvas.tsx +278 -0
- package/skills/struq/recipes/components/struq/HorizontalGallery.tsx +106 -0
- package/skills/struq/recipes/components/struq/MagneticButton.tsx +92 -0
- package/skills/struq/recipes/components/struq/MediaFrame.tsx +96 -0
- package/skills/struq/recipes/components/struq/PinnedZoom.tsx +79 -0
- package/skills/struq/recipes/components/struq/Reveal.tsx +64 -0
- package/skills/struq/recipes/components/struq/SmoothScroll.tsx +57 -0
- package/skills/struq/recipes/components/struq/SplitHeadline.tsx +68 -0
- package/skills/struq/recipes/components/struq/scenes/contours.ts +161 -0
- package/skills/struq/recipes/components/struq/scenes/dots.ts +110 -0
- package/skills/struq/recipes/components/struq/scenes/ripple.ts +99 -0
- package/skills/struq/recipes/components/struq/scenes/types.ts +114 -0
- package/skills/struq/recipes/lib/gsap.ts +21 -0
- package/skills/struq/references/anti-patterns.md +71 -6
- package/skills/struq/references/baseline-prompt.md +18 -14
- package/skills/struq/references/brief-router.md +7 -8
- package/skills/struq/references/composition.md +0 -1
- package/skills/struq/references/depth-parallax.md +1 -1
- package/skills/struq/references/direction.md +5 -5
- package/skills/struq/references/hero-scenes.md +107 -0
- package/skills/struq/references/iconography.md +1 -1
- package/skills/struq/references/imagery.md +10 -29
- package/skills/struq/references/micro-interactions.md +1 -1
- package/skills/struq/references/mobile-composition.md +1 -1
- package/skills/struq/references/niche-packets.md +338 -0
- package/skills/struq/references/page-architecture.md +2 -2
- package/skills/struq/references/pinning-scrub.md +3 -4
- package/skills/struq/references/scroll-reveals.md +1 -2
- package/skills/struq/references/sections.md +141 -0
- package/skills/struq/references/seo.md +2 -2
- package/skills/struq/references/showcase.md +2 -2
- package/skills/struq/references/svg-motion.md +390 -390
- package/skills/struq/references/typography.md +39 -39
- package/skills/struq/references/vibe-presets.md +5 -5
- package/skills/struq/references/webgl-scenes.md +593 -593
- package/skills/struq/scripts/canon.mjs +309 -0
- package/skills/struq/scripts/check.mjs +547 -0
- package/skills/struq/scripts/font-names.mjs +40 -0
- package/skills/struq/scripts/images.mjs +360 -0
- package/skills/struq/scripts/log.mjs +92 -0
- package/skills/struq/scripts/packs.mjs +156 -0
- package/skills/struq/scripts/recipes.mjs +135 -0
- /package/{bin → cli}/struq.mjs +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// lib/gsap.ts (Struq recipe)
|
|
2
|
+
// One registry for every GSAP plugin the site uses. Import gsap from here, never from "gsap"
|
|
3
|
+
// directly, so a component rendered on its own still has its plugins registered.
|
|
4
|
+
import gsap from "gsap";
|
|
5
|
+
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
|
6
|
+
import { SplitText } from "gsap/SplitText";
|
|
7
|
+
import { useGSAP } from "@gsap/react";
|
|
8
|
+
|
|
9
|
+
gsap.registerPlugin(ScrollTrigger, SplitText, useGSAP);
|
|
10
|
+
|
|
11
|
+
// The Struq entrance: expo.out inside the reveal band.
|
|
12
|
+
gsap.defaults({ ease: "expo.out", duration: 0.9 });
|
|
13
|
+
|
|
14
|
+
export const EASE = {
|
|
15
|
+
enter: "expo.out",
|
|
16
|
+
exit: "power2.in",
|
|
17
|
+
swap: "power3.inOut",
|
|
18
|
+
curtain: "power4.inOut",
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
export { gsap, ScrollTrigger, SplitText, useGSAP };
|
|
@@ -44,7 +44,7 @@ Surface ladder steps (`surface-0 → 1 → 2`) signal depth tonally. Real shadow
|
|
|
44
44
|
**Agency:** kill the cards; let type and rhythm carry it:
|
|
45
45
|
```tsx
|
|
46
46
|
<section className="content-grid py-40 gap-y-0">
|
|
47
|
-
<
|
|
47
|
+
<h2 className="font-display text-h2 max-w-[18ch]">What we take on, start to finish</h2>
|
|
48
48
|
<div className="mt-16 divide-y divide-line">
|
|
49
49
|
{features.map((f) => (
|
|
50
50
|
<div key={f.title} className="group grid grid-cols-12 items-baseline gap-4 py-10">
|
|
@@ -55,7 +55,7 @@ Surface ladder steps (`surface-0 → 1 → 2`) signal depth tonally. Real shadow
|
|
|
55
55
|
</div>
|
|
56
56
|
</section>
|
|
57
57
|
```
|
|
58
|
-
Editorial rows, left-aligned, divided by hairlines, asymmetric columns. Hierarchy comes from type scale and the
|
|
58
|
+
Editorial rows, left-aligned, divided by hairlines, asymmetric columns. Hierarchy comes from type scale and the hairlines, not from a label or a number on every row (see §16). ([composition.md](composition.md))
|
|
59
59
|
|
|
60
60
|
---
|
|
61
61
|
|
|
@@ -180,7 +180,7 @@ Lines rising from individual overflow masks. ([scroll-reveals.md](scroll-reveals
|
|
|
180
180
|
**Reflex:** h1 `text-4xl`, h2 `text-3xl`, h3 `text-2xl`, h4 `text-xl`: five steps between 20 and 36px, all `font-bold`, default tracking.
|
|
181
181
|
**The tell:** compressed mid-range hierarchy. Nothing leads, everything is "kind of big."
|
|
182
182
|
|
|
183
|
-
**Agency:** two extremes, set through the role tokens so nothing is improvised: `font-display text-display` for the hero (the token carries the pairing's clamp, tracking and leading), `font-display text-h2` per section, `type-h3 text-h3` for items, `text-body` at 1 to 1.0625rem, and `
|
|
183
|
+
**Agency:** two extremes, set through the role tokens so nothing is improvised: `font-display text-display` for the hero (the token carries the pairing's clamp, tracking and leading), `font-display text-h2` per section, `type-h3 text-h3` for items, `text-body` at 1 to 1.0625rem, and quiet `text-small text-ink-muted` meta lines (sentence case) for captions and dates. No label above a heading (§19). No `tracking-tight`: tracking belongs to the face and the role ([typography.md](typography.md) §3.1). **Hero/display runs light (300 to 400)**. Large + light is the agency signal; heavy display (600+) is a deliberate genre choice only, never the reflex. **Floor:** body ≥1rem, muted ≥0.875rem, nothing below 12px. **Contrast:** body ≥4.5:1 (WCAG AA), large/≥24px ≥3:1; `ink-faint` is never text. ([baseline-prompt.md](baseline-prompt.md); the Signature pairings, roles and fluid scale are in [typography.md](typography.md))
|
|
184
184
|
|
|
185
185
|
---
|
|
186
186
|
|
|
@@ -216,7 +216,7 @@ Lines rising from individual overflow masks. ([scroll-reveals.md](scroll-reveals
|
|
|
216
216
|
**Reflex:** `01 / 02 / 03 / 04` stamped on every section header, every feature, every list row (often oversized in a corner) as the page's wayfinding system.
|
|
217
217
|
**The tell:** uniform sequencing is reflexive, not editorial. No art director numbers *everything*; it is the same impulse as `py-16` on every section, applied to labels. It signals "I needed a decoration and reached for the nearest one."
|
|
218
218
|
|
|
219
|
-
**Agency:** wayfinding through variety:
|
|
219
|
+
**Agency:** wayfinding through variety: hairline rules, scale jumps, surface changes, the headings themselves. Never tracked uppercase eyebrows: they are a tell of their own (§19). A numeral earns its place only as a **single deliberate accent** (one oversized `03` against a manifesto line), never as a per-row counter. If you can delete the numbers and lose no meaning, they were filler.
|
|
220
220
|
|
|
221
221
|
**The boundary:** ordinal numbering *inside one genuinely stepped component* (the phase kickers of a pinned sequence, see [pinning-scrub.md](pinning-scrub.md) §1, or a numbered process that really is ordered) is legitimate content, not this tell. The tell is *page-wide* stamping: numerals as the default decoration on every section, feature, and row.
|
|
222
222
|
|
|
@@ -229,7 +229,7 @@ Lines rising from individual overflow masks. ([scroll-reveals.md](scroll-reveals
|
|
|
229
229
|
<p className="font-mono text-xs uppercase tracking-widest">// System online</p>
|
|
230
230
|
<span className="font-mono text-sm">[ 2024 ]</span>
|
|
231
231
|
```
|
|
232
|
-
**The tell:** monospace on
|
|
232
|
+
**The tell:** monospace on labels, stats, badges, timestamps, or body to look "technical / engineered / terminal." It is the aesthetic of a code editor, not a designed page, and it has become a default reach for "techy," which makes it read as generated.
|
|
233
233
|
|
|
234
234
|
**Agency:** a real text face carries all UI and copy; "precision" comes from tracking, `tabular-nums` on figures, and tight type, not from a typewriter font. **Mono is reserved for literal code** (snippets, inline `code`, terminal output on a dev-tool site where it is genuinely the subject). Anywhere else, it is a tell. The same logic kills the scramble/decode "terminal" loader ([loaders.md](loaders.md)).
|
|
235
235
|
|
|
@@ -249,8 +249,72 @@ Lines rising from individual overflow masks. ([scroll-reveals.md](scroll-reveals
|
|
|
249
249
|
|
|
250
250
|
---
|
|
251
251
|
|
|
252
|
+
## 19. Eyebrows
|
|
253
|
+
|
|
254
|
+
**Reflex:**
|
|
255
|
+
```tsx
|
|
256
|
+
<p className="text-xs font-medium uppercase tracking-[0.18em] text-ink-muted">Restaurant in Amsterdam-Noord</p>
|
|
257
|
+
<h1 className="font-display text-display">Vis en groente, van het seizoen.</h1>
|
|
258
|
+
```
|
|
259
|
+
**The tell:** a small uppercase tracked label above every heading. Generated pages put one on every section because it looks "designed" at no cost; the result is the most recognisable AI marketing page there is, and it competes with the heading it sits on.
|
|
260
|
+
|
|
261
|
+
**Agency:** the heading carries the section alone. Wayfinding comes from a hairline rule (`border-t border-line`), a change of surface step, a jump in scale, an image. A place or a date the reader needs goes in the lead line under the heading, in sentence case. `check.mjs` fails on `uppercase` with `tracking-`.
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## 20. Gradients, tinted fills and glass
|
|
266
|
+
|
|
267
|
+
**Reflex:** `bg-linear-to-b from-surface-0 to-surface-2` on a section, `bg-accent/10` behind a label, `bg-white/5 backdrop-blur` on a card, a radial glow behind the hero.
|
|
268
|
+
|
|
269
|
+
**The tell:** colour that is neither a surface nor the accent. Translucent tints and gradients are the generator's way to make a flat page "rich"; they muddy the palette and read as a template on sight.
|
|
270
|
+
|
|
271
|
+
**Agency:** flat, opaque surface steps (`bg-surface-1`, `bg-surface-2`) and hairlines. The only translucent fill is a flat black scrim over a photo so type in `text-on-photo` reads. Depth comes from the WebGL hero, the photography and the overlap of sections, not from washes.
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## 21. Pills
|
|
276
|
+
|
|
277
|
+
**Reflex:** `rounded-full px-4 py-1.5 bg-accent/10 text-accent` badges, a "New" pill with a pulsing dot, pill-shaped buttons.
|
|
278
|
+
|
|
279
|
+
**The tell:** the pill with a dot is the badge of every generated launch page. Pills everywhere also break the site's radius system.
|
|
280
|
+
|
|
281
|
+
**Agency:** the site radius (0 to 4px) on every button and frame; status as words in the lead line; no badges. The primary CTA is a `MagneticButton`, square-cornered, its fill rising on hover.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## 22. Empty frames
|
|
286
|
+
|
|
287
|
+
**Reflex:** an `aspect-[16/9] bg-surface-2` box with "Foto volgt" in it, or a placeholder component standing in for a photo that never came.
|
|
288
|
+
|
|
289
|
+
**The tell:** the reader sees the site was not finished, and a plain page with no images at all beats it.
|
|
290
|
+
|
|
291
|
+
**Agency:** every frame holds a real image: the client's photo, or the Struq beeldbank pack (`scripts/images.mjs`, automatic, CC0), or, when struq.nl is unreachable, the graded fallback plate the script writes. Never ship an empty frame ([struq-imagery.md](struq-imagery.md)).
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## 23. The smaller tells
|
|
296
|
+
|
|
297
|
+
Each is cheap on its own; three of them together read as generated.
|
|
298
|
+
|
|
299
|
+
| Tell | Reflex | Instead |
|
|
300
|
+
|---|---|---|
|
|
301
|
+
| Side-stripe borders | `border-l-4 border-accent` down a card or a quote | a hairline, or nothing |
|
|
302
|
+
| Status badges | `bg-green-100 text-green-700` "Available" chips | the fact in the lead line |
|
|
303
|
+
| Live dots | an `animate-ping` dot beside "Now open" | the words alone |
|
|
304
|
+
| Uppercase marquees | a scrolling band of tracked capitals | a large display line in sentence case, if any marquee at all |
|
|
305
|
+
| Generic CTAs | "Learn more", "Get started", "Lees meer" | a verb and the thing: "Reserveer een tafel", "Bekijk de projecten" |
|
|
306
|
+
| Generic headings | "Our Services", "Why Choose Us", "Over ons" | a heading only this business could write |
|
|
307
|
+
| Hype copy | "seamless", "elevate", "next-level", "naadloos", "revolutionair" | the plain, specific claim from the brief |
|
|
308
|
+
| Hero-metric template | a hero followed by a strip of four big numbers | numbers only when the brief gives them, set inside the copy |
|
|
309
|
+
| Category-reflex colours | green for every eco brand, blue for every bank, purple for every AI | the canon palette that fits the mood; the reflex hue only when it is the brand's own |
|
|
310
|
+
| Restated headings | a paragraph that repeats the heading in more words | the paragraph adds the next fact |
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
252
314
|
## The 30-second tell scan
|
|
253
315
|
|
|
316
|
+
The script does this scan for you: `node <skill-dir>/scripts/check.mjs` prints every hit with its file and line. By hand:
|
|
317
|
+
|
|
254
318
|
Before shipping, run these grep-level checks:
|
|
255
319
|
- `shadow-md|shadow-lg` on cards → §1 · `from-blue|to-purple|bg-clip-text` → §11
|
|
256
320
|
- `py-16` ≥3 times → §8 · `grid-cols-3` of identical children → §2
|
|
@@ -262,4 +326,5 @@ Before shipping, run these grep-level checks:
|
|
|
262
326
|
- `font-bold` on the hero/`h1` / largest size = `text-4xl` → §12 · faint text on body copy / contrast below AA → §12
|
|
263
327
|
- size ladders like `text-2xl md:text-4xl lg:text-6xl`, `px-`/`py-` tuned per breakpoint, fixed-`px` layout widths, or no fluid root font-size → §18
|
|
264
328
|
- `text-4xl` as the largest size on the page → §12 · "Lorem|Acme|John Doe" → §15
|
|
265
|
-
- `
|
|
329
|
+
- `uppercase` with `tracking-`, or an `eyebrow`/`kicker` class → §19 · `gradient|bg-linear-|backdrop-blur|bg-accent/` → §20 · `rounded-full` with `px-` → §21 · `Placeholder`, an empty `aspect-` div → §22
|
|
330
|
+
- `tracking-tight`, `text-ink-faint`, or `uppercase` with `tracking-` anywhere (the eyebrow tell, §19), or `text-xs` on headings (role tokens bypassed) → §12 · `Inter|Roboto|Open_Sans|Poppins|Montserrat|Lato|Space_Grotesk|Geist` in `next/font` imports → [typography.md](typography.md) §3.4
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# The Minimal Baseline: rationale + standalone paste block
|
|
2
2
|
|
|
3
|
-
**The
|
|
3
|
+
**SKILL.md carries the short form of this baseline ("The taste baseline") and the Build checklist that enforces it.** This file does two jobs. It explains **why each rule earns its place** (the rationale below), and it carries the one **standalone copy-paste block** for agents that do not load Struq as a skill.
|
|
4
4
|
|
|
5
|
-
<!--
|
|
5
|
+
<!-- The paste block below must agree with SKILL.md ("The taste baseline" and "Three rules that never bend"). When a rule changes there, change it here in the same commit. -->
|
|
6
6
|
|
|
7
|
-
## The paste block (
|
|
7
|
+
## The paste block (for any LLM, any stack)
|
|
8
8
|
|
|
9
9
|
Paste this at the top of any project prompt to lift the output into the top tier. Include the rationale section only when the agent has room.
|
|
10
10
|
|
|
@@ -28,10 +28,11 @@ about half that. Body 1rem to 1.0625rem at 400, leading 1.6+, 55 to 75ch;
|
|
|
28
28
|
emphasis 500, never 700. Hierarchy from scale contrast, not medium sizes.
|
|
29
29
|
ROLES: every string takes a role token, never text-xs, text-lg or text-5xl:
|
|
30
30
|
font-display text-display, font-display text-h2, type-h3 text-h3,
|
|
31
|
-
text-lead, text-body, text-small,
|
|
31
|
+
text-lead, text-body, text-small, meta text-meta, font-ui text-ui,
|
|
32
32
|
stat text-stat, and font-display text-statement for the one Orient line
|
|
33
|
-
or manifesto.
|
|
34
|
-
|
|
33
|
+
or manifesto. NO EYEBROWS: no small uppercase tracked label above a
|
|
34
|
+
heading, no uppercase marquee. The heading, a hairline and scale do the
|
|
35
|
+
wayfinding; captions and dates are meta in sentence case, ink-muted.
|
|
35
36
|
FLOOR: body at least 1rem, muted at least 0.875rem, nothing under 12px.
|
|
36
37
|
CONTRAST: body at least 4.5:1, large text at least 3:1. ink-faint is never
|
|
37
38
|
text: hairlines and disabled states only.
|
|
@@ -65,8 +66,10 @@ flat gray hex. Shadows only for true floating elements, then large, soft and fai
|
|
|
65
66
|
COLOR: One accent, used sparingly (links, the key CTA, one highlight per
|
|
66
67
|
viewport). Neutrals are temperature-shifted (warm cream and off-black, or cool
|
|
67
68
|
charcoal), never pure #FFF and #000 with gray-500 text. A second accent is a
|
|
68
|
-
declared genre choice with its own token and the same budget.
|
|
69
|
-
|
|
69
|
+
declared genre choice with its own token and the same budget. NO gradients
|
|
70
|
+
of any kind, no gradient text, no glass, no tinted translucent fills (a
|
|
71
|
+
flat black scrim over a photo is the one exception), no pills, no pulsing
|
|
72
|
+
dots. No blue or purple unless the brand is that colour.
|
|
70
73
|
|
|
71
74
|
MOTION: Lenis smooth scroll (lerp 0.09 to 0.11) on one gsap.ticker clock,
|
|
72
75
|
unless the genre calls for native scroll. Every entrance animates opacity AND
|
|
@@ -90,20 +93,21 @@ ink-colored. Never emoji, never the AI-cliche icons (Sparkles, Rocket, Zap),
|
|
|
90
93
|
never icons in colored chips. Animated icons run under 500ms on click and
|
|
91
94
|
never loop in standard UI.
|
|
92
95
|
|
|
93
|
-
DETAILS THAT SELL IT: Real running copy
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
DETAILS THAT SELL IT: Real running copy, never invented facts. Never
|
|
97
|
+
01/02/03 stamped on every section (one deliberate oversized numeral is
|
|
98
|
+
fine). Never an empty image frame. The footer is designed as hard as the
|
|
99
|
+
hero. Custom
|
|
96
100
|
::selection color. Visible, designed focus-visible states. Every route change
|
|
97
101
|
is a designed transition, not a hard cut.
|
|
98
102
|
```
|
|
99
103
|
|
|
100
|
-
The block is the floor, not the ceiling. For the full recipe stack (
|
|
104
|
+
The block is the floor, not the ceiling. For the full recipe stack (Struq's canon, the beeldbank, the Lenis + GSAP single clock, the SplitText hero, a showpiece, the WebGL hero layer) run the Build checklist in [SKILL.md](../SKILL.md).
|
|
101
105
|
|
|
102
106
|
## Why each rule matters
|
|
103
107
|
|
|
104
108
|
**Spacing asymmetry.** Uniform `py-16` is the single most reliable template tell. Real art direction treats vertical rhythm as pacing: a hero breathes, a feature block is dense, a manifesto line sits in 20rem of air. Varying section padding by role is free and reads instantly as "someone decided this." The "too empty" instruction exists because LLMs systematically under-use whitespace; what feels wasteful in code feels expensive in the browser.
|
|
105
109
|
|
|
106
|
-
**Type scale contrast.** Templates use five sizes between 16 and 36px, so nothing leads. Agencies compress hierarchy into two extremes: enormous display type (it is the visual, not decoration around the visual) and small, quiet everything else. Negative tracking matters because text faces are spaced for 16px, and at 96px+ the gaps look broken; how much depends on the face (a geometric sans takes about -0.03em, a hairline serif about -0.01em), which is why tracking lives in the pairing row and not in one `tracking-tight`. The
|
|
110
|
+
**Type scale contrast.** Templates use five sizes between 16 and 36px, so nothing leads. Agencies compress hierarchy into two extremes: enormous display type (it is the visual, not decoration around the visual) and small, quiet everything else. Negative tracking matters because text faces are spaced for 16px, and at 96px+ the gaps look broken; how much depends on the face (a geometric sans takes about -0.03em, a hairline serif about -0.01em), which is why tracking lives in the pairing row and not in one `tracking-tight`. The fix for the "medium subhead" that flattens hierarchy is not a tracked-uppercase eyebrow (that is a tell of its own) but a bigger gap between display and body. Role tokens (`text-display`, `text-h2`, `meta text-meta`) exist so no component improvises a size, and the one signature move exists so two Struq sites never open the same way. The deeper type system (Signature pairings, roles, fluid scale, SplitText typography) lives in [typography.md](typography.md).
|
|
107
111
|
|
|
108
112
|
**Tonal elevation.** Drop shadows on light cards are the Bootstrap-era elevation model; design-led products (Linear, Vercel, Arc) moved to tonal ladders, where closer surfaces are lighter (dark mode) or distinguished by hairline borders and a subtle tone step (light mode). The light-mode direction is a preset decision: editorial/luxury pages are the lightest layer with panels stepping deeper; brutalist pages are toned with white panels lifting off them. One step per layer keeps depth legible either way. When you do shadow a floating element, the large/soft/low-opacity recipe reads as ambient light, while `shadow-lg` reads as a 2015 component library.
|
|
109
113
|
|
|
@@ -115,4 +119,4 @@ The block is the floor, not the ceiling. For the full recipe stack (two-face typ
|
|
|
115
119
|
|
|
116
120
|
**Grid-breaking.** A layout where every element respects the container communicates "theme." Bleed, overlap, and stickiness communicate "art direction" because they require coordinating elements across section boundaries, which is exactly what templates cannot do. Twice per page is the floor; it should look intentional, not chaotic.
|
|
117
121
|
|
|
118
|
-
**Details.** Real copy forces real layout decisions (lorem hides widow/orphan and width problems).
|
|
122
|
+
**Details.** Real copy forces real layout decisions (lorem hides widow/orphan and width problems). Long pages get their editorial spine from hairline rules, surface steps and headings that say something, never from tracked eyebrow labels. Note what is *removed* here: stamping `01/02/03` on every section/feature/row is one of the most reliable AI tells. It is reflexive wayfinding that no editor would apply uniformly. One deliberate oversized numeral can be a strong accent; sequencing the whole page is filler. (Ordinal numbering *inside* one genuinely stepped component, such as a pinned phase sequence or a numbered process, is legitimate; page-wide stamping is the tell.) The same logic bans two other reflexes: **monospace as flavor** (it is the look of a code editor, not a designed page, and belongs only around literal code) and the **cliché icon set** (`Sparkles`/`Rocket`/`Zap` and their emoji forms 🚀✨💡 are the visual signature of generated UI). The footer is where everyone quits, so finishing it signals the whole build is finished. Selection color and focus states are tiny, but they are the difference between "surface-level styling" and "someone touched every state."
|
|
@@ -82,8 +82,8 @@ Steps marked **SYSTEM** are decided in [direction.md](direction.md) §4; they ar
|
|
|
82
82
|
7. **Route transitions.** Multi-page: plan a full-screen transition and its variant ([route-transitions.md](route-transitions.md), with [page-entrances.md](page-entrances.md) for what happens after the curtain). One-pager: none, but reserve the hook if routes are coming. Loader yes or no from the genre's register (loaders suit narrative, portfolio and launch only, see [loaders.md](loaders.md)).
|
|
83
83
|
8. **WebGL gate.** Only if a section genuinely calls for it AND the deadline allows. If yes, plan the poster first and name the moment (what the user sees, where in the arc, the fallback, one gate for canvas and pin) per [webgl-r3f.md](webgl-r3f.md) §1 and [webgl-scenes.md](webgl-scenes.md) §5. **No model file?** Walk the ladder in [webgl-scenes.md](webgl-scenes.md) §4 ("No model file yet"): ask for the GLB in the gate, a declared primitive stand-in built to be replaced, or a downgrade to photography or a type-led showpiece; label the rung under ASSUMPTIONS. A whole page as one directed scene is [../cinematic/README.md](../cinematic/README.md) instead.
|
|
84
84
|
9. **Motion, icons, imagery grade.** SYSTEM. Motion is the [SKILL.md](../SKILL.md) baseline unless a deviation is recorded with its reason.
|
|
85
|
-
10. **Copy budget.** Per section, from [page-architecture.md](page-architecture.md) §5: hero h1 at most 8 words, eyebrows
|
|
86
|
-
11. **Shot list.** Per section: subject, grade, ratio and source (real, generate, treated stock), mapped onto the arc. The grade and ratio set come from SYSTEM; missing imagery is
|
|
85
|
+
10. **Copy budget.** Per section, from [page-architecture.md](page-architecture.md) §5: hero h1 at most 8 words, no eyebrows, section headlines at most 10, body 2 to 4 sentences at 44 to 52ch, CTAs 2 to 4 verb-first words, exactly one manifesto line on the whole page.
|
|
86
|
+
11. **Shot list.** Per section: subject, grade, ratio and source (real, generate, treated stock), mapped onto the arc. The grade and ratio set come from SYSTEM; missing imagery is filled from the Struq beeldbank pack (struq-imagery.md) per [imagery.md](imagery.md) §4b.
|
|
87
87
|
|
|
88
88
|
---
|
|
89
89
|
|
|
@@ -147,7 +147,7 @@ Stack: Next.js App Router · React · Tailwind · GSAP · Lenis, each with the i
|
|
|
147
147
|
## MATERIALS
|
|
148
148
|
Have: black wordmark (SVG requested), roastery photo set (bar room, hands and pour, roaster machine)
|
|
149
149
|
Missing: all copy (written from the first draft, no lorem); roast bag and bean shots
|
|
150
|
-
(
|
|
150
|
+
(filled from the beeldbank pack proof, struq-imagery.md); no numbers or quotes (no proof faked)
|
|
151
151
|
|
|
152
152
|
## CONCEPT
|
|
153
153
|
Idea: the site reads like a roast log: one week, one batch, written down by people who taste everything
|
|
@@ -160,8 +160,8 @@ Niche: food and drink product brand, buyer = the person buying beans · genre Pr
|
|
|
160
160
|
· preset Warm Editorial · borrowed: none (the tasting bar is a room, but imagery is not a
|
|
161
161
|
borrowable field; Warm Editorial already carries the warmth)
|
|
162
162
|
Display: Fraunces 300, SOFT 50 (Google Fonts, OFL) · pairing S2
|
|
163
|
-
Text: Urbanist 400 body, 500
|
|
164
|
-
· roles per typography.md §3.1 S2: display 300, h2 300, h3 400,
|
|
163
|
+
Text: Urbanist 400 body, 500 UI (Google Fonts, OFL)
|
|
164
|
+
· roles per typography.md §3.1 S2: display 300, h2 300, h3 400, body 400
|
|
165
165
|
Signature move: soft-scrub (the roast date headline softens as the reader leaves the hero)
|
|
166
166
|
Hero entrance: Warm Editorial's soft-scrub plus line rise
|
|
167
167
|
Palette: light · --surface-0 #FAF6F0 --surface-1 #F3EDE2 --surface-2 #EAE1D2 --surface-3 #DFD3BF
|
|
@@ -198,7 +198,7 @@ Copy budget: h1 at most 8 words ("Roasted Tuesday. Gone by Friday."), manifesto
|
|
|
198
198
|
line only, roast frames = name + 2 tasting words + origin, CTAs "Visit the bar", "Get the beans"
|
|
199
199
|
Assets to produce (8 images):
|
|
200
200
|
1 hero bean macro, 21/9, generate on-brief
|
|
201
|
-
4 roast bags, 1/1, shoot (
|
|
201
|
+
4 roast bags, 1/1, shoot (beeldbank pack images until then)
|
|
202
202
|
1 bar room, 21/9, existing set, regraded
|
|
203
203
|
1 hands and pour, 4/5, existing set, regraded
|
|
204
204
|
1 roaster machine, 4/5, existing set, regraded
|
|
@@ -208,8 +208,7 @@ Assets to produce (8 images):
|
|
|
208
208
|
- "best roaster in the city" read as craft-serious: one accent (the preset's oxblood, not the
|
|
209
209
|
cream-plus-terracotta reflex), no second accent
|
|
210
210
|
- wordmark stays black on cream (ink #1E1A16)
|
|
211
|
-
- the gallery
|
|
212
|
-
static 2 by 2 grid of placeholder frames (no pin), and the pin switches on when they land
|
|
211
|
+
- the gallery runs on beeldbank pack images until the 4 roast shots exist, then swaps them in
|
|
213
212
|
- shop deferred: row 6 collects newsletter sign-ups instead of a cart
|
|
214
213
|
- one-pager now, /shop reserved in the nav
|
|
215
214
|
```
|
|
@@ -65,7 +65,6 @@ The arithmetic: trespasser gets `-mb-32` + `relative z-10`; receiver gets `pt-56
|
|
|
65
65
|
|
|
66
66
|
```tsx
|
|
67
67
|
<section className="content-grid gap-y-10 py-32">
|
|
68
|
-
<p className="eyebrow text-eyebrow text-ink-muted">Selected Work</p>
|
|
69
68
|
<h2 className="font-display text-h2">Selected projects</h2>
|
|
70
69
|
<p className="text-body max-w-[52ch] text-ink-muted">…</p>
|
|
71
70
|
<img className="bleed-full aspect-[21/9] w-full object-cover" src="/wide.jpg" alt="" />
|
|
@@ -168,7 +168,7 @@ export function StackingCards({ items }: { items: { kicker: string; title: strin
|
|
|
168
168
|
<section key={item.title} data-card
|
|
169
169
|
className={`sticky top-0 flex flex-col justify-between gap-16 overflow-hidden
|
|
170
170
|
px-8 py-12 md:min-h-[100svh] ${SURFACES[i % SURFACES.length]}`}>
|
|
171
|
-
<span className="
|
|
171
|
+
<span className="text-small text-ink-muted">{item.kicker}</span>
|
|
172
172
|
<div className="max-w-3xl">
|
|
173
173
|
<h3 className="font-display text-h2">{item.title}</h3>
|
|
174
174
|
<p className="text-lead mt-6 max-w-[48ch] text-ink-muted">{item.body}</p>
|
|
@@ -13,7 +13,7 @@ This file is the orchestrator for **Build**, **Redesign** and the planning half
|
|
|
13
13
|
| 2 | **Concept** | CONCEPT | this file §3; the tension from [brief-router.md](brief-router.md) §1 | never |
|
|
14
14
|
| 3 | **System** | SYSTEM | this file §4 (sole owner of type, palette, icons, imagery, motion) | never |
|
|
15
15
|
| 4 | **Site plan** | PLAN | [brief-router.md](brief-router.md) §2 | never |
|
|
16
|
-
| Gate | **
|
|
16
|
+
| Gate | **Approval** | one message: summary, materials request, "Akkoord?" | this file §5 | never: it is the one stop, and there is no second |
|
|
17
17
|
| 5 | **Build** | the site | [../SKILL.md](../SKILL.md) workflow steps 2 to 7 | |
|
|
18
18
|
| 6 | **Verify** | VERIFY | [../SKILL.md](../SKILL.md) workflow step 8 | never |
|
|
19
19
|
|
|
@@ -43,13 +43,13 @@ What MATERIALS tracks:
|
|
|
43
43
|
- No biography, training, process or sourcing stated as fact ("trained as a production thrower", "eats at the restaurant before drawing", "clay dug in Alentejo") unless the brief says it.
|
|
44
44
|
- No catalogue facts: invented works, sizes, glazes, materials, prices, editions, sold-out states, stockists or client names.
|
|
45
45
|
|
|
46
|
-
When the layout needs that copy to exist, draft it as
|
|
46
|
+
When the layout needs that copy to exist, draft it as plain layout copy and list every drafted claim under ASSUMPTIONS ("Studio paragraph: drafted, replace with Ana's words"; "Work index: 8 entries drafted as layout copy, no prices, no client names"). A portfolio with no work list gets its entries drafted the same way, titled as layout copy, never as the record. The gate message names the drafts so the client can replace them.
|
|
47
47
|
|
|
48
48
|
**No real imagery yet.** Plan for it instead of papering over it:
|
|
49
49
|
|
|
50
|
-
-
|
|
50
|
+
- Media-led showpieces work from the first build: the beeldbank pack supplies the photos. Never stage one on empty frames.
|
|
51
51
|
- Every missing image becomes a shot-list line (subject, grade, ratio) under PLAN `Assets to produce`, so it can be shot, sourced or generated later ([imagery.md](imagery.md) §4).
|
|
52
|
-
- In the build,
|
|
52
|
+
- In the build, every image slot is filled from the Struq beeldbank pack ([struq-imagery.md](struq-imagery.md)), automatically and without asking: the images are CC0. A frame is never empty and never a placeholder.
|
|
53
53
|
|
|
54
54
|
## 2. Brand
|
|
55
55
|
|
|
@@ -103,7 +103,7 @@ Decide in this order:
|
|
|
103
103
|
|
|
104
104
|
Write `STRUQ.md` (template below), then send one message with: the name, the idea line, the two typefaces and pairing id, the signature move and hero entrance, the palette as hex, the icon family, the arc as one line, the showpiece ids, the open assumptions, and the materials request. End it with exactly this:
|
|
105
105
|
|
|
106
|
-
> **
|
|
106
|
+
> **Akkoord? Heb je een logo, foto's of echte cijfers, stuur ze nu.**
|
|
107
107
|
|
|
108
108
|
Apply edits and new materials to the file; do not re-interview. This is the only stop between the brief and the build (the wizard in [brief-router.md](brief-router.md) §1b is the one exception, and it runs before this gate, not instead of it).
|
|
109
109
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Hero scenes: the WebGL layer behind the hero
|
|
2
|
+
|
|
3
|
+
Every Struq marketing site carries a WebGL layer in its hero. `components/struq/HeroCanvas.tsx` owns everything that makes such a layer safe in production; a scene owns only what it draws.
|
|
4
|
+
|
|
5
|
+
| HeroCanvas owns | The scene owns |
|
|
6
|
+
|---|---|
|
|
7
|
+
| Loading three.js in its own chunk after first paint | Its `THREE.Scene` and camera |
|
|
8
|
+
| The poster (the photo, or the flat surface) and the fade-in after the first frame | Its geometry, material and uniforms |
|
|
9
|
+
| The DPR cap (1.75, phones 1.5) and a 2.4 million pixel budget | Reading the palette it is given |
|
|
10
|
+
| Pausing offscreen and in a hidden tab | `update(state)`: time, pointer, scroll, velocity into uniforms |
|
|
11
|
+
| One composed still frame under `prefers-reduced-motion` and on phones | `resize(w, h)`, an optional multi-pass `render(renderer)`, `dispose()` |
|
|
12
|
+
| The quiet zones: up to four `[data-quiet]` rects behind the copy, in device pixels | Calling `hush(v, gl_FragCoord.xy)` on its intensity |
|
|
13
|
+
| The pointer's `presence`, easing in on move and out on rest | Using it as a gentle modulation only |
|
|
14
|
+
| Running on `gsap.ticker`, the page's one clock | |
|
|
15
|
+
| Keeping the poster when WebGL or the scene fails | |
|
|
16
|
+
|
|
17
|
+
## The three shipped scenes
|
|
18
|
+
|
|
19
|
+
| Scene | Looks like | Needs | Suits |
|
|
20
|
+
|---|---|---|---|
|
|
21
|
+
| `contours` | contour lines drifting like a slow survey map, every sixth in the accent | nothing | architecture, studios, consultancy, nature, research |
|
|
22
|
+
| `ripple` | the hero photo as a liquid surface, rippling under the pointer, pushing in on scroll | `image` | hospitality, food, hotels, fashion, wellness |
|
|
23
|
+
| `dots` | a field of points rolling like a swell, a hill under the pointer | nothing | mobility, logistics, technology, sport, energy |
|
|
24
|
+
|
|
25
|
+
Props: `scene`, `image` (poster, and the texture for ripple), `grade` (0 to 1, how far ripple pulls the photo into the palette), `scrim` (0 to 0.6, a flat black layer for type in `text-on-photo`), `priority` (the poster is the LCP image).
|
|
26
|
+
|
|
27
|
+
## The craft bar
|
|
28
|
+
|
|
29
|
+
What separates a high-end ambient hero from a screensaver, and what every shipped scene does:
|
|
30
|
+
|
|
31
|
+
- **Clean:** fine marks (hairlines, small dots) on a flat ground, antialiased with `fwidth`, and where marks crowd closer than the screen can draw, they fade into a mist instead of moire.
|
|
32
|
+
- **Deep:** a perspective field (a horizon just above the top edge or in the upper third), marks compressing and thinning toward it, a fog toward the far side.
|
|
33
|
+
- **Calm behind the copy:** `hush` holds the field under a soft ceiling behind every `data-quiet` element, so the copy reads at 4.5:1 and the field still runs on behind it.
|
|
34
|
+
- **Composed:** the field is weighted to the right or the vanishing point sits on the right third; the left, where the headline sits, is the quiet side.
|
|
35
|
+
- **Slow:** the ambient clock is 0.02 to 0.06 of real time. The pointer only modulates (a soft lift, a ripple) and only while it is present.
|
|
36
|
+
- **Cheap:** noise runs once per field cell into a small target, never per screen pixel, where the look allows (`contours` shows the two-pass pattern).
|
|
37
|
+
|
|
38
|
+
## Writing a scene (or porting the MCP background)
|
|
39
|
+
|
|
40
|
+
A scene is one file in `components/struq/scenes/` that exports `createScene`:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// components/struq/scenes/<name>.ts
|
|
44
|
+
import { NOISE_GLSL, QUIET_GLSL, quietUniforms, type CreateScene } from "./types";
|
|
45
|
+
|
|
46
|
+
export const createScene: CreateScene = async ({ THREE, renderer, palette, image, grade, dark }) => {
|
|
47
|
+
const scene = new THREE.Scene();
|
|
48
|
+
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
49
|
+
const quiet = quietUniforms(THREE, dark ? 0.08 : 0.12);
|
|
50
|
+
const uniforms = {
|
|
51
|
+
...quiet.uniforms,
|
|
52
|
+
uTime: { value: 0 },
|
|
53
|
+
uRes: { value: new THREE.Vector2(1, 1) },
|
|
54
|
+
uPointer: { value: new THREE.Vector2() },
|
|
55
|
+
uScroll: { value: 0 },
|
|
56
|
+
uSurface: { value: new THREE.Color(palette.surface) },
|
|
57
|
+
uInk: { value: new THREE.Color(palette.ink) },
|
|
58
|
+
uAccent: { value: new THREE.Color(palette.accent) },
|
|
59
|
+
};
|
|
60
|
+
const material = new THREE.ShaderMaterial({
|
|
61
|
+
uniforms,
|
|
62
|
+
vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = vec4(position.xy, 0.0, 1.0); }`,
|
|
63
|
+
fragmentShader: `
|
|
64
|
+
uniform float uTime; uniform vec2 uRes; uniform vec2 uPointer; uniform float uScroll;
|
|
65
|
+
uniform vec3 uSurface; uniform vec3 uInk; uniform vec3 uAccent;
|
|
66
|
+
varying vec2 vUv;
|
|
67
|
+
${NOISE_GLSL}
|
|
68
|
+
${QUIET_GLSL}
|
|
69
|
+
void main() {
|
|
70
|
+
float v = 0.0; // ... the look's intensity, from noise per cell where you can
|
|
71
|
+
v = hush(v, gl_FragCoord.xy);
|
|
72
|
+
vec3 col = mix(uSurface, uInk, v); // the three palette colours only
|
|
73
|
+
gl_FragColor = vec4(col, 1.0);
|
|
74
|
+
#include <colorspace_fragment>
|
|
75
|
+
}`,
|
|
76
|
+
});
|
|
77
|
+
const geometry = new THREE.PlaneGeometry(2, 2);
|
|
78
|
+
scene.add(new THREE.Mesh(geometry, material));
|
|
79
|
+
return {
|
|
80
|
+
scene,
|
|
81
|
+
camera,
|
|
82
|
+
update(s) { uniforms.uTime.value = s.time; uniforms.uPointer.value.set(s.pointer.x, s.pointer.y); uniforms.uScroll.value = s.scroll; quiet.update(s); },
|
|
83
|
+
resize(w, h) { uniforms.uRes.value.set(w, h); },
|
|
84
|
+
dispose() { geometry.dispose(); material.dispose(); },
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Then register it in `HeroCanvas.tsx`:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
const SCENES = {
|
|
93
|
+
contours: () => import("./scenes/contours"),
|
|
94
|
+
ripple: () => import("./scenes/ripple"),
|
|
95
|
+
dots: () => import("./scenes/dots"),
|
|
96
|
+
"<name>": () => import("./scenes/<name>"),
|
|
97
|
+
};
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**The MCP background (`struq_achtergrond`).** It returns a background family with its shader or scene description and its parameters. Port it into this contract: its shader becomes the fragment shader, its colour inputs become `uSurface`, `uInk` and `uAccent` built from `palette` (that is the recolour: the site's tokens, never the colours in the MCP sample), its animation reads `uTime`, and its pointer or scroll inputs read `uPointer` and `uScroll`. Build it even when you cannot watch it: if it fails to compile, HeroCanvas logs the error and keeps the poster, so the hero is never empty. Check the browser console when you do have a browser.
|
|
101
|
+
|
|
102
|
+
## Rules that keep it fast
|
|
103
|
+
|
|
104
|
+
- Colours are linear inside the shader; end every fragment shader with `#include <colorspace_fragment>` and set `texture.colorSpace = THREE.SRGBColorSpace` on photo textures, or the palette renders too dark.
|
|
105
|
+
- One full-screen pass, at most five noise octaves, no loops over pixels. Points: up to about 15,000.
|
|
106
|
+
- One HeroCanvas per page. A second canvas further down is a deliberate WebGL moment ([webgl-r3f.md](webgl-r3f.md)), not a reflex.
|
|
107
|
+
- Never a glossy 3D blob, a purple glow or a mesh-gradient wash: the scene works in the palette's neutrals with the accent as a thread.
|
|
@@ -103,7 +103,7 @@ import { ArrowRight, Search } from "@struq/icons";
|
|
|
103
103
|
- **Name clashes.** `Link`, `Image`, `File` and `Text` collide with `next/link`, `next/image` and DOM globals; import the `LinkIcon`, `ImageIcon`, `FileIcon` and `TextIcon` aliases instead.
|
|
104
104
|
- **Accent off in one area:** `data-icon-accent="off"` on a parent paints the accent in the base colour (needs `icons.css`). `data-icons="live"` keeps the accent on at rest in an area when `states.css` is loaded.
|
|
105
105
|
- **Status icons** (one tone) take their colour from the place they sit in: they follow `currentColor`, or a token through `color="var(--color-...)"`, never a hex.
|
|
106
|
-
- **Without React:** `sprite.svg` (copy it to `public/`, `<use href="/sprite.svg#search" />`, two-tone, no hover motion), plain SVG files under `svg/`, or `iconify.json` (prefix `struq`, for Iconify loaders in Vue, Svelte, Astro or Tailwind). Same family, same rules.
|
|
106
|
+
- **Without React:** `sprite.svg` (copy it to `public/`, `<use href="/sprite.svg#search" />`, two-tone, no hover motion), plain SVG files under `svg/`, or `iconify.json` (prefix `struq`, for Iconify loaders in Vue, Svelte, Astro or Tailwind: the icon in the text colour with the accent at half strength, no hover motion, and `struq:<name>-draw` for a draw-in that plays once and ignores reduced motion). Same family, same rules.
|
|
107
107
|
- **A missing icon** is drawn to the set's rules (24 grid, round caps and joins, the site's stroke, base and accent as two parts) or the whole site moves to row 3. Never patch the gap with a Phosphor glyph.
|
|
108
108
|
|
|
109
109
|
**Never Lucide as the reflex.** It is the default of every generated UI, so it reads as
|
|
@@ -23,6 +23,7 @@ Pick one:
|
|
|
23
23
|
Apply the grade in one reusable layer, not per-image in an editor:
|
|
24
24
|
|
|
25
25
|
```tsx
|
|
26
|
+
// The grade as a component. On a Struq build MediaFrame is this layer: add the filter to its <Image> className.
|
|
26
27
|
// components/ui/GradedImage.tsx: every content image renders through this
|
|
27
28
|
export function GradedImage({ src, alt, ratio = "4/3", className = "" }: {
|
|
28
29
|
src: string; alt: string; ratio?: string; className?: string;
|
|
@@ -31,15 +32,13 @@ export function GradedImage({ src, alt, ratio = "4/3", className = "" }: {
|
|
|
31
32
|
<div className={`relative overflow-hidden ${className}`} style={{ aspectRatio: ratio }}>
|
|
32
33
|
<img src={src} alt={alt} className="h-full w-full scale-[1.18] object-cover
|
|
33
34
|
[filter:saturate(0.82)_contrast(1.05)]" />
|
|
34
|
-
{/* the tint: surface temperature washed over every image */}
|
|
35
|
-
<div aria-hidden className="absolute inset-0 bg-surface-0/15 mix-blend-color" />
|
|
36
35
|
</div>
|
|
37
36
|
);
|
|
38
37
|
}
|
|
39
38
|
```
|
|
40
39
|
|
|
41
|
-
- The `filter`
|
|
42
|
-
- Duotone variant: `[filter:grayscale(1)]` on the img
|
|
40
|
+
- The `filter` values are **per-project constants**: tune once against the preset, then never per-image. (Static CSS filters are fine; it's *scrubbed* filters that cost, see [setup.md](setup.md) §5.)
|
|
41
|
+
- Duotone variant: `[filter:grayscale(1)_sepia(0.2)]` on the img. Never a tinted translucent layer over the photo: that is a tell of its own ([anti-patterns.md](anti-patterns.md) §20).
|
|
43
42
|
- `scale-[1.18]` is the parallax headroom from [depth-parallax.md](depth-parallax.md) §2, so grading and panning share the same frame component.
|
|
44
43
|
|
|
45
44
|
**Cheap tell:** every image at its source colors, six looks on one page. **Instead:** One `GradedImage` component; swap the page's grade by editing one filter line.
|
|
@@ -97,38 +96,20 @@ In order of preference:
|
|
|
97
96
|
- Avoid the AI-render look: no glossy 3D blobs, no purple-glow gradients, no hyperdetailed-everything. The §1 grade pass over generated output hides most remaining tells.
|
|
98
97
|
4. **Treated stock**: last resort, and only *through* the grade layer. Untreated stock (beaming laptop people, handshake photos) is below the floor.
|
|
99
98
|
|
|
100
|
-
**Beeldbank, stock and generated images show context, never the client's product.** The city, the canal, a rider, a room, a material: yes. A stranger's e-bike, a generated bottle or a stock ceramic presented as the product: never, because the visitor reads it as the thing they would buy. Until the real product is shot, it is a
|
|
99
|
+
**Beeldbank, stock and generated images show context, never the client's product.** The city, the canal, a rider, a room, a material: yes. A stranger's e-bike, a generated bottle or a stock ceramic presented as the product: never, because the visitor reads it as the thing they would buy. Until the real product is shot, it is type, a context image of the client's world, a declared 3D stand-in ([webgl-scenes.md](webgl-scenes.md) §4, "No model file yet"), or type.
|
|
101
100
|
|
|
102
101
|
**Never:** browser-chrome screenshot mockups tilted in perspective, generic "dashboard glow" renders, or watermark-adjacent placeholder imagery. If a section has no honest image, the strongest move is **no image**: type, space, and surface carry it (same logic as the no-icon rule, [iconography.md](iconography.md) §1).
|
|
103
102
|
|
|
104
103
|
---
|
|
105
104
|
|
|
106
|
-
## 4b.
|
|
105
|
+
## 4b. Never an empty frame
|
|
107
106
|
|
|
108
|
-
A slot that will hold
|
|
107
|
+
A slot that will one day hold the client's own photo holds a beeldbank image today. There is no placeholder state on a Struq site: no grey box, no tonal frame with a "photo to come" caption, no placeholder component.
|
|
109
108
|
|
|
110
|
-
- **
|
|
111
|
-
- **
|
|
112
|
-
- **
|
|
113
|
-
- **Markup:** `
|
|
114
|
-
- **Never inside a showpiece.** A media-led showpiece is not staged on placeholders; it waits for the real assets ([showcase.md](showcase.md) Choosing).
|
|
115
|
-
|
|
116
|
-
```tsx
|
|
117
|
-
// components/ui/ImagePlaceholder.tsx: the stand-in for one shot-list line
|
|
118
|
-
export function ImagePlaceholder({ caption, ratio = "4/3", className = "" }: {
|
|
119
|
-
caption: string; ratio?: string; className?: string;
|
|
120
|
-
}) {
|
|
121
|
-
return (
|
|
122
|
-
<div role="img" aria-label={`Planned image: ${caption}`}
|
|
123
|
-
className={`relative overflow-hidden bg-linear-to-b from-surface-2 to-surface-3 ${className}`}
|
|
124
|
-
style={{ aspectRatio: ratio }}>
|
|
125
|
-
<span className="eyebrow text-eyebrow absolute bottom-4 left-4 text-ink-muted">
|
|
126
|
-
{caption}
|
|
127
|
-
</span>
|
|
128
|
-
</div>
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
```
|
|
109
|
+
- **Default:** `node <skill-dir>/scripts/images.mjs <pack>` fills every slot from the pack that matches the brief ([struq-imagery.md](struq-imagery.md)). It runs on every build, without asking: the images are free and CC0.
|
|
110
|
+
- **struq.nl unreachable:** the same script writes graded fallback plates in the site palette (two planes of light, a horizon hairline, grain) at each slot's ratio, and `lib/struq-images.ts` points at them. They are abstract images, not boxes. Run the script again when the network is back and the photos replace them in place.
|
|
111
|
+
- **The client's product, team or projects:** a beeldbank photo never stands in for those. Those sections use type, the WebGL layer or a context image of the client's world (the material, the place), and the gap is listed under Assumptions.
|
|
112
|
+
- **Markup:** every image goes through `MediaFrame`, which requires an `image`, so an empty frame cannot be written by accident. `check.mjs` fails on placeholder components, "photo to come" captions and empty `aspect-` boxes.
|
|
132
113
|
|
|
133
114
|
---
|
|
134
115
|
|
|
@@ -120,7 +120,7 @@ export function Cursor() {
|
|
|
120
120
|
<div ref={dot} className="pointer-events-none fixed left-0 top-0 z-[100] -translate-x-1/2 -translate-y-1/2">
|
|
121
121
|
<div className={`flex items-center justify-center rounded-full bg-ink text-surface-0
|
|
122
122
|
transition-all duration-300 ease-out-expo
|
|
123
|
-
${variant === "view" ? "
|
|
123
|
+
${variant === "view" ? "text-small h-20 w-20" : "h-2.5 w-2.5"}`}>
|
|
124
124
|
{variant === "view" && "View"}
|
|
125
125
|
</div>
|
|
126
126
|
</div>
|
|
@@ -11,7 +11,7 @@ The principle: **mobile keeps the attitude, not the geometry.** Asymmetry, tresp
|
|
|
11
11
|
| Desktop technique | Mobile translation | What survives |
|
|
12
12
|
|---|---|---|
|
|
13
13
|
| **Layered lockup** ([composition.md](composition.md) §3): type and image sharing grid cells | Stack, but keep a *deliberate small overlap*: headline pulls `-mb-[0.5em]` over the image top (or image pulls under the headline's last line). Drop `mix-blend-difference` if the overlap zone gets busy at small width; a `pointer-events-none` scrim or plain ink type is more legible | the trespass |
|
|
14
|
-
| **Asymmetric 12-col rows** (features as editorial rows, [anti-patterns.md](anti-patterns.md) §2) | Single column with **alternating indents** (`even:pl-[10vw]`) or a hanging
|
|
14
|
+
| **Asymmetric 12-col rows** (features as editorial rows, [anti-patterns.md](anti-patterns.md) §2) | Single column with **alternating indents** (`even:pl-[10vw]`) or a hanging meta column (`grid-cols-[5rem_1fr]`, a year or a place in `text-small`). Never centered | the off-axis rhythm |
|
|
15
15
|
| **Section bleed / seam** ([composition.md](composition.md) §1) | Keep it: negative margins are width-independent. Reduce overlap depth ~50% (`-mb-16` not `-mb-32`) so the seam doesn't eat a whole viewport | the trespass |
|
|
16
16
|
| **Full-bleed breakout** ([composition.md](composition.md) §2) | The `content-grid` collapses gracefully (side lanes hit their 1.5rem floor), but full-bleed only *contrasts* if contained content is visibly inset. Keep body at the `content` lane; never let everything go edge-to-edge | contained vs escaping |
|
|
17
17
|
| **Off-canvas ghost type** ([composition.md](composition.md) §4) | Shrink to ~24-30vw and keep the crop, or drop it. At 18vw-of-360px it's a smudge: if it can't be huge, it shouldn't exist | scale contrast (or nothing) |
|