@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.
Files changed (129) hide show
  1. package/README.md +22 -36
  2. package/package.json +3 -2
  3. package/skills/struq/SKILL.md +139 -307
  4. package/skills/struq/blocks/CATALOG.md +426 -0
  5. package/skills/struq/blocks/backgrounds/background-aurora.tsx +818 -0
  6. package/skills/struq/blocks/backgrounds/background-facets.tsx +1283 -0
  7. package/skills/struq/blocks/backgrounds/background-silk.tsx +906 -0
  8. package/skills/struq/blocks/catalog.json +1302 -0
  9. package/skills/struq/blocks/footers/footer-contact.tsx +789 -0
  10. package/skills/struq/blocks/footers/footer-reveal.tsx +313 -0
  11. package/skills/struq/blocks/headers/header-bar.tsx +652 -0
  12. package/skills/struq/blocks/headers/menu-curtain.tsx +790 -0
  13. package/skills/struq/blocks/heroes/hero-framed.tsx +627 -0
  14. package/skills/struq/blocks/heroes/hero-index.tsx +855 -0
  15. package/skills/struq/blocks/heroes/hero-poster.tsx +1086 -0
  16. package/skills/struq/blocks/heroes/hero-split.tsx +685 -0
  17. package/skills/struq/blocks/heroes/hero-still.tsx +862 -0
  18. package/skills/struq/{recipes/components/struq → blocks/kit}/Cursor.tsx +1 -1
  19. package/skills/struq/{recipes/components/struq → blocks/kit}/HeroCanvas.tsx +2 -2
  20. package/skills/struq/{recipes/components/struq → blocks/kit}/HorizontalGallery.tsx +2 -2
  21. package/skills/struq/{recipes/components/struq → blocks/kit}/MagneticButton.tsx +1 -1
  22. package/skills/struq/{recipes/components/struq → blocks/kit}/MediaFrame.tsx +2 -2
  23. package/skills/struq/{recipes/components/struq → blocks/kit}/PinnedZoom.tsx +14 -4
  24. package/skills/struq/{recipes/components/struq → blocks/kit}/Reveal.tsx +1 -1
  25. package/skills/struq/{recipes/components/struq → blocks/kit}/SmoothScroll.tsx +1 -1
  26. package/skills/struq/{recipes/components/struq → blocks/kit}/SplitHeadline.tsx +1 -1
  27. package/skills/struq/blocks/kit/blocks.css +2102 -0
  28. package/skills/struq/blocks/kit/images.ts +17 -0
  29. package/skills/struq/blocks/kit/photo-read.ts +102 -0
  30. package/skills/struq/blocks/page-transitions/page-transition-gate.tsx +706 -0
  31. package/skills/struq/blocks/page-transitions/page-transition-slices.tsx +469 -0
  32. package/skills/struq/blocks/page-transitions/page-transition-stack.tsx +583 -0
  33. package/skills/struq/blocks/preloaders/preloader-gate.tsx +358 -0
  34. package/skills/struq/blocks/preloaders/preloader-slices-uncovered.ts +50 -0
  35. package/skills/struq/blocks/preloaders/preloader-slices.tsx +289 -0
  36. package/skills/struq/blocks/preloaders/preloader-stack.tsx +367 -0
  37. package/skills/struq/blocks/preloaders/preloader-wordmark.tsx +568 -0
  38. package/skills/struq/blocks/sections/about-studio.tsx +436 -0
  39. package/skills/struq/blocks/sections/approach.tsx +661 -0
  40. package/skills/struq/blocks/sections/contact-form.tsx +664 -0
  41. package/skills/struq/blocks/sections/cta-closing.tsx +763 -0
  42. package/skills/struq/blocks/sections/fanned-cards.tsx +547 -0
  43. package/skills/struq/blocks/sections/film-strip.tsx +385 -0
  44. package/skills/struq/blocks/sections/grid-assembly.tsx +616 -0
  45. package/skills/struq/blocks/sections/hover-list.tsx +467 -0
  46. package/skills/struq/blocks/sections/image-trail.tsx +432 -0
  47. package/skills/struq/blocks/sections/manifesto.tsx +691 -0
  48. package/skills/struq/blocks/sections/panel-accordion.tsx +380 -0
  49. package/skills/struq/blocks/sections/reveals.tsx +416 -0
  50. package/skills/struq/blocks/sections/split-pair.tsx +670 -0
  51. package/skills/struq/blocks/sections/sticky-chapters.tsx +256 -0
  52. package/skills/struq/blocks/sections/velocity-type.tsx +269 -0
  53. package/skills/struq/{cinematic/showpieces/toonstuk-fill-rows.tsx → blocks/showpieces/fill-rows.tsx} +1 -1
  54. package/skills/struq/{cinematic/showpieces/toonstuk-hero-opener.tsx → blocks/showpieces/hero-opener.tsx} +5 -5
  55. package/skills/struq/{cinematic/showpieces/toonstuk-image-to-fullbleed.tsx → blocks/showpieces/image-to-fullbleed.tsx} +16 -5
  56. package/skills/struq/blocks/showpieces/manifesto-scrub.tsx +48 -0
  57. package/skills/struq/{cinematic/showpieces/toonstuk-panel-wipe.tsx → blocks/showpieces/panel-wipe.tsx} +2 -2
  58. package/skills/struq/{cinematic/showpieces/toonstuk-pixel-resolve.tsx → blocks/showpieces/pixel-resolve.tsx} +1 -1
  59. package/skills/struq/{cinematic/showpieces/toonstuk-project-stack.tsx → blocks/showpieces/project-stack.tsx} +2 -2
  60. package/skills/struq/blocks/showpieces/scrub-connector.tsx +307 -0
  61. package/skills/struq/{cinematic/showpieces/toonstuk-wordmark-finale.tsx → blocks/showpieces/wordmark-finale.tsx} +1 -1
  62. package/skills/struq/blocks/transitions/zoom-through.tsx +613 -0
  63. package/skills/struq/canon/canon.json +627 -0
  64. package/skills/struq/cinematic/README.md +7 -18
  65. package/skills/struq/cinematic/examples/code/nextjs/app/components/LenisRoot.tsx +1 -1
  66. package/skills/struq/cinematic/examples/code/shared/styles.css +1 -1
  67. package/skills/struq/cinematic/examples/code/vite/src/LenisRoot.tsx +1 -1
  68. package/skills/struq/cinematic/references/01-core-mental-model.md +1 -1
  69. package/skills/struq/cinematic/references/02-vocabulary.md +1 -1
  70. package/skills/struq/cinematic/references/07-stack-integration.md +2 -2
  71. package/skills/struq/recipes/app/fonts.ts +12 -22
  72. package/skills/struq/recipes/app/globals.css +34 -177
  73. package/skills/struq/recipes/app/layout.tsx +13 -11
  74. package/skills/struq/references/craft.md +77 -0
  75. package/skills/struq/references/iconography.md +28 -230
  76. package/skills/struq/references/stack-targets.md +23 -74
  77. package/skills/struq/references/tells.md +52 -0
  78. package/skills/struq/references/triage.md +6 -7
  79. package/skills/struq/scripts/blocks.mjs +156 -0
  80. package/skills/struq/scripts/canon.mjs +106 -281
  81. package/skills/struq/scripts/check.mjs +92 -126
  82. package/skills/struq/scripts/images.mjs +2 -2
  83. package/skills/struq/scripts/log.mjs +6 -23
  84. package/skills/struq/scripts/tokens.mjs +222 -0
  85. package/skills/struq/cinematic/showpieces/README.md +0 -18
  86. package/skills/struq/cinematic/showpieces/toonstuk-manifesto-scrub.tsx +0 -81
  87. package/skills/struq/cinematic/showpieces/toonstuk-scrub-connector.tsx +0 -78
  88. package/skills/struq/references/anti-patterns.md +0 -330
  89. package/skills/struq/references/baseline-prompt.md +0 -122
  90. package/skills/struq/references/brand.md +0 -143
  91. package/skills/struq/references/brief-router.md +0 -218
  92. package/skills/struq/references/composition.md +0 -190
  93. package/skills/struq/references/depth-parallax.md +0 -235
  94. package/skills/struq/references/direction.md +0 -188
  95. package/skills/struq/references/feel-layer.md +0 -107
  96. package/skills/struq/references/gsap-api.md +0 -293
  97. package/skills/struq/references/hero-scenes.md +0 -107
  98. package/skills/struq/references/imagery.md +0 -125
  99. package/skills/struq/references/loaders.md +0 -287
  100. package/skills/struq/references/micro-interactions.md +0 -270
  101. package/skills/struq/references/mobile-composition.md +0 -70
  102. package/skills/struq/references/nav-header.md +0 -296
  103. package/skills/struq/references/niche-packets.md +0 -338
  104. package/skills/struq/references/page-architecture.md +0 -134
  105. package/skills/struq/references/page-entrances.md +0 -198
  106. package/skills/struq/references/performance.md +0 -235
  107. package/skills/struq/references/pinning-scrub.md +0 -340
  108. package/skills/struq/references/pitfalls.md +0 -290
  109. package/skills/struq/references/route-transitions.md +0 -281
  110. package/skills/struq/references/scaling.md +0 -151
  111. package/skills/struq/references/scroll-reveals.md +0 -279
  112. package/skills/struq/references/sections.md +0 -141
  113. package/skills/struq/references/seo.md +0 -219
  114. package/skills/struq/references/setup.md +0 -397
  115. package/skills/struq/references/showcase.md +0 -87
  116. package/skills/struq/references/struq-imagery.md +0 -107
  117. package/skills/struq/references/svg-motion.md +0 -390
  118. package/skills/struq/references/transitions.md +0 -160
  119. package/skills/struq/references/typography.md +0 -720
  120. package/skills/struq/references/vibe-presets.md +0 -287
  121. package/skills/struq/references/webgl-r3f.md +0 -274
  122. package/skills/struq/references/webgl-scenes.md +0 -593
  123. package/skills/struq/scripts/font-names.mjs +0 -40
  124. package/skills/struq/scripts/recipes.mjs +0 -135
  125. /package/skills/struq/{recipes/lib → blocks/kit}/gsap.ts +0 -0
  126. /package/skills/struq/{recipes/components/struq → blocks/kit}/scenes/contours.ts +0 -0
  127. /package/skills/struq/{recipes/components/struq → blocks/kit}/scenes/dots.ts +0 -0
  128. /package/skills/struq/{recipes/components/struq → blocks/kit}/scenes/ripple.ts +0 -0
  129. /package/skills/struq/{recipes/components/struq → blocks/kit}/scenes/types.ts +0 -0
@@ -21,7 +21,7 @@ export function LenisRoot({ children }: LenisRootProps): JSX.Element {
21
21
  useEffect(() => {
22
22
  // Registered unconditionally; the instance is resolved through the ref on
23
23
  // every frame. Never capture it once and early-return: that is the
24
- // dead-scroll race (references/setup.md §2).
24
+ // dead-scroll race.
25
25
  const update = (time: number) => {
26
26
  lenisRef.current?.lenis?.raf(time * 1000);
27
27
  };
@@ -55,7 +55,7 @@ a {
55
55
  }
56
56
 
57
57
  /* Demo values. In a Struq build the h1 takes `font-display text-display` and the
58
- pairing's tracking from typography.md §3.1; the clamp keeps a rem term for zoom. */
58
+ pairing's tracking; the clamp keeps a rem term for zoom. */
59
59
  .experience-copy h1 {
60
60
  margin: 0;
61
61
  font-size: clamp(2.875rem, 1.2rem + 6.86vw, 8rem);
@@ -16,7 +16,7 @@ export function LenisRoot({ children }: LenisRootProps): JSX.Element {
16
16
  useLenis(() => ScrollTrigger.update());
17
17
 
18
18
  useEffect(() => {
19
- // Registered unconditionally, instance resolved per frame (references/setup.md §2).
19
+ // Registered unconditionally, instance resolved per frame.
20
20
  const update = (time: number) => {
21
21
  lenisRef.current?.lenis?.raf(time * 1000);
22
22
  };
@@ -118,7 +118,7 @@ These transitions should have explicit entry, active, and exit states.
118
118
 
119
119
  ## One clock, several signals
120
120
 
121
- There is exactly one frame clock. Lenis is advanced from `gsap.ticker`, ScrollTrigger reads from Lenis, and the R3F canvas is advanced from the same ticker (see [`../../references/setup.md`](../../references/setup.md) and [`../../references/webgl-r3f.md`](../../references/webgl-r3f.md)). Nothing in the experience runs its own `requestAnimationFrame` loop. Scroll progress and elapsed time are different signals read on that one clock, not different clocks.
121
+ There is exactly one frame clock. Lenis is advanced from `gsap.ticker`, ScrollTrigger reads from Lenis, and the R3F canvas is advanced from the same ticker (the kit's `SmoothScroll` and [`../examples/code/shared/GsapFrameLoop.tsx`](../examples/code/shared/GsapFrameLoop.tsx)). Nothing in the experience runs its own `requestAnimationFrame` loop. Scroll progress and elapsed time are different signals read on that one clock, not different clocks.
122
122
 
123
123
  At minimum, distinguish:
124
124
 
@@ -138,7 +138,7 @@ Offsetting related elements. A stagger should communicate reading order, directi
138
138
  Mapping scroll progress to a timeline playhead.
139
139
 
140
140
  ### Smoothing
141
- Reducing abrupt changes in a signal. Lenis smoothing, ScrollTrigger scrub smoothing, and manual damping should be assigned intentionally rather than stacked without control. Pair them the way the main skill does in [`../../references/feel-layer.md`](../../references/feel-layer.md): lower the Lenis lerp, lower the scrub number, and use `scrub: true` for attached layers and a small number for floating ones.
141
+ Reducing abrupt changes in a signal. Lenis smoothing, ScrollTrigger scrub smoothing, and manual damping should be assigned intentionally rather than stacked without control. Pair them: lower the Lenis lerp, lower the scrub number, and use `scrub: true` for attached layers and a small number for floating ones.
142
142
 
143
143
  ### Easing
144
144
  The mathematical shape of a known transition between values.
@@ -73,7 +73,7 @@ Key integration responsibilities:
73
73
  - Lenis is stopped during states that require scroll lock.
74
74
  - native nested scrolling is handled deliberately.
75
75
 
76
- Lenis must be created with `autoRaf: false`. If it keeps its own animation loop it becomes a second clock next to `gsap.ticker`, and scroll, ScrollTrigger and the canvas drift apart by a frame. The canonical setup lives in [`../../references/setup.md`](../../references/setup.md); this is the conceptual shape.
76
+ Lenis must be created with `autoRaf: false`. If it keeps its own animation loop it becomes a second clock next to `gsap.ticker`, and scroll, ScrollTrigger and the canvas drift apart by a frame. The canonical setup is the kit's `SmoothScroll` (`blocks/kit/SmoothScroll.tsx`); this is the conceptual shape.
77
77
 
78
78
  Conceptual setup:
79
79
 
@@ -102,7 +102,7 @@ Do not add CSS `scroll-behavior: smooth` on top of Lenis.
102
102
 
103
103
  ## R3F frame-loop options
104
104
 
105
- The single-clock rule applies: the canvas is rendered from `gsap.ticker`, the same callback chain that advances Lenis. The exact bridge is documented in [`../../references/webgl-r3f.md`](../../references/webgl-r3f.md) and shown in [`../examples/code/shared/GsapFrameLoop.tsx`](../examples/code/shared/GsapFrameLoop.tsx).
105
+ The single-clock rule applies: the canvas is rendered from `gsap.ticker`, the same callback chain that advances Lenis. The exact bridge is shown in [`../examples/code/shared/GsapFrameLoop.tsx`](../examples/code/shared/GsapFrameLoop.tsx).
106
106
 
107
107
  ### Ticker-driven loop (default)
108
108
 
@@ -1,22 +1,12 @@
1
- // app/fonts.ts (Struq recipe)
2
- // Exactly two faces: a display face and a text face. Swap these two imports for the pairing you
3
- // picked in SKILL.md ("Pick the two faces"); keep the variable names --face-display and --face-body,
4
- // globals.css maps them to font-display and font-body.
5
- // Default: S2, Fraunces (soft serif display) over Urbanist (round geometric text).
6
- import { Fraunces, Urbanist } from "next/font/google";
7
-
8
- export const display = Fraunces({
9
- subsets: ["latin"],
10
- axes: ["SOFT", "opsz"],
11
- variable: "--face-display",
12
- display: "swap",
13
- });
14
-
15
- export const body = Urbanist({
16
- subsets: ["latin"],
17
- variable: "--face-body",
18
- display: "swap",
19
- });
20
-
21
- /** Every face's CSS variable, for the <html> className in app/layout.tsx. */
22
- export const fontVariables = [display, body].map((f) => f.variable).join(" ");
1
+ // app/fonts.ts (Struq recipe). canon.mjs type <pairing> rewrites this file; the default is serif-editorial.
2
+ // Each Google face is asked for its real weight range, then as the bare family as a fallback, because Google refuses a
3
+ // request for a weight the family lacks. The layout puts one <link> per URL in <head>; the token block in globals.css
4
+ // names the faces (--face-display, --face-body, --face-accent), so the blocks measure with the real face.
5
+ export const fontHrefs: string[] = [
6
+ "https://fonts.googleapis.com/css2?family=Fraunces:wght@100..900&display=swap",
7
+ "https://fonts.googleapis.com/css2?family=Fraunces&display=swap",
8
+ "https://fonts.googleapis.com/css2?family=Urbanist:wght@100..900&display=swap",
9
+ "https://fonts.googleapis.com/css2?family=Urbanist&display=swap",
10
+ "https://fonts.googleapis.com/css2?family=Fraunces:ital,wght@1,100..900&display=swap",
11
+ "https://fonts.googleapis.com/css2?family=Fraunces:ital@1&display=swap",
12
+ ];
@@ -1,185 +1,42 @@
1
1
  @import "tailwindcss";
2
+ /* The block kit: type roles, the motion gate (.sq-js with its 3 s fallback), the contrast scenes and every block's
3
+ own CSS, all scoped to [data-blocks] (on <body>). */
4
+ @import "../components/struq/kit/blocks.css";
2
5
 
3
- /* ==========================================================================
4
- STRUQ TOKENS. Set the values from the palette you chose in SKILL.md.
5
- Keep the names: every recipe and the hero canvas read them.
6
- ========================================================================== */
7
- :root {
8
- --surface-0: #f5efe6; /* the page */
9
- --surface-1: #ede4d6; /* one step: panels, the showpiece ground */
10
- --surface-2: #e1d4bf; /* two steps: image wells, hover grounds */
11
- --surface-3: #cdbb9f; /* three steps: hairline accents, never text */
12
- --ink: #1e1a15; /* headlines and body */
13
- --ink-muted: #5d5446; /* secondary text, at least 4.5:1 on surface-0 */
14
- --accent: #8a3b22; /* the one accent: the key CTA, one highlight per screen */
15
- --accent-ink: #fbf6ee; /* text on the accent */
16
- --line: rgb(30 26 21 / 0.12); /* hairlines: the ink at low alpha, never a grey hex */
17
- --on-photo: #f7f3ec; /* type set over a photo or the ripple hero, on light and dark presets alike */
18
- --inverse: #1e1a15; /* the contrast scene's ground: the palette's inverse role, else the ink */
19
- --on-inverse: #f5efe6; /* type on it: else the page ground */
20
- --field: #8a3b22; /* a full-strength colour field: the palette's field role, else the accent */
21
- --on-field: #fbf6ee; /* type on it */
22
- --display-weight: 300; /* light display: 300, or 400 for a face without a 300 */
23
- }
24
-
6
+ /* The theme names the blocks use that the kit leaves to the site. */
25
7
  @theme inline {
26
- --color-surface-0: var(--surface-0);
27
- --color-surface-1: var(--surface-1);
28
- --color-surface-2: var(--surface-2);
29
- --color-surface-3: var(--surface-3);
30
- --color-ink: var(--ink);
31
- --color-ink-muted: var(--ink-muted);
32
8
  --color-accent: var(--accent);
33
- --color-accent-ink: var(--accent-ink);
34
- --color-line: var(--line);
35
- --color-on-photo: var(--on-photo);
36
- --color-inverse: var(--inverse);
37
- --color-on-inverse: var(--on-inverse);
38
- --color-field: var(--field);
39
- --color-on-field: var(--on-field);
9
+ --color-accent-2: var(--accent-2);
10
+ --color-photo-ground: var(--photo-ground);
40
11
  --font-display: var(--face-display), ui-serif, Georgia, serif;
41
- --font-body: var(--face-body), ui-sans-serif, system-ui, sans-serif;
42
- --font-accent: var(--face-accent, var(--face-display)), ui-serif, Georgia, serif;
43
- --font-sans: var(--face-body), ui-sans-serif, system-ui, sans-serif;
44
- }
45
-
46
- @theme {
47
- /* Type roles: write text-display, text-h2, text-h3, text-lead, text-body, text-small. Never text-xl or text-5xl. */
48
- --text-display: clamp(2.875rem, 1.2rem + 6.86vw, 8rem);
49
- --text-display--line-height: 0.95;
50
- --text-display--letter-spacing: -0.025em;
51
- --text-h2: clamp(2.25rem, 1.3rem + 3.4vw, 4.75rem);
52
- --text-h2--line-height: 1.04;
53
- --text-h2--letter-spacing: -0.015em;
54
- --text-h3: clamp(1.375rem, 1.15rem + 0.8vw, 1.875rem);
55
- --text-h3--line-height: 1.2;
56
- --text-lead: clamp(1.125rem, 1.05rem + 0.3vw, 1.3125rem);
57
- --text-lead--line-height: 1.5;
58
- --text-body: 1.0625rem;
59
- --text-body--line-height: 1.65;
60
- --text-small: 0.875rem;
61
- --text-small--line-height: 1.5;
62
- --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
63
- --ease-in-out-quint: cubic-bezier(0.83, 0, 0.17, 1);
64
- }
65
-
66
- /* ==========================================================================
67
- THE CONTRAST SCENE. Every site has one, at most two: wrap a section in scene-inverse (the
68
- palette's dark ground with light type) or scene-field (a full-strength colour field). The tokens
69
- are remapped inside, so every recipe in it turns with it.
70
- ========================================================================== */
71
- .scene-inverse {
72
- --surface-0: var(--inverse);
73
- --surface-1: color-mix(in oklch, var(--inverse), var(--on-inverse) 6%);
74
- --surface-2: color-mix(in oklch, var(--inverse), var(--on-inverse) 12%);
75
- --surface-3: color-mix(in oklch, var(--inverse), var(--on-inverse) 22%);
76
- --ink: var(--on-inverse);
77
- --ink-muted: color-mix(in oklch, var(--on-inverse) 72%, var(--inverse));
78
- --line: color-mix(in oklch, var(--on-inverse) 18%, transparent);
79
- background-color: var(--surface-0);
80
- color: var(--ink);
81
- }
82
- .scene-field {
83
- --surface-0: var(--field);
84
- --surface-1: color-mix(in oklch, var(--field), var(--on-field) 8%);
85
- --surface-2: color-mix(in oklch, var(--field), var(--on-field) 14%);
86
- --surface-3: color-mix(in oklch, var(--field), var(--on-field) 24%);
87
- --ink: var(--on-field);
88
- --ink-muted: color-mix(in oklch, var(--on-field) 78%, var(--field));
89
- --line: color-mix(in oklch, var(--on-field) 22%, transparent);
90
- --accent: var(--on-field);
91
- --accent-ink: var(--field);
92
- background-color: var(--surface-0);
93
- color: var(--ink);
94
- }
95
-
96
- @layer base {
97
- /* The hero opener's handover: the section after it slides up over the pinned opener. */
98
- [data-showpiece="hero-opener"] + *,
99
- .pin-spacer:has(> [data-showpiece="hero-opener"]) + * {
100
- position: relative;
101
- z-index: 2;
102
- background-color: var(--surface-0);
103
- }
104
- /* The one scaling knob: 16px at 390, about 17.6px at 1440, 18px from about 1640. */
105
- html {
106
- font-size: clamp(1rem, 0.92rem + 0.2vw, 1.125rem);
107
- background: var(--surface-0);
108
- color: var(--ink);
109
- }
110
- body {
111
- font-family: var(--font-body);
112
- font-size: var(--text-body);
113
- line-height: 1.65;
114
- -webkit-font-smoothing: antialiased;
115
- text-rendering: optimizeLegibility;
116
- }
117
- h1, h2, h3, .font-display {
118
- font-family: var(--font-display);
119
- font-weight: var(--display-weight);
120
- text-wrap: balance;
121
- }
122
- p {
123
- text-wrap: pretty;
124
- }
125
- ::selection {
126
- background: var(--accent);
127
- color: var(--accent-ink);
128
- }
129
- :focus-visible {
130
- outline: 2px solid var(--accent);
131
- outline-offset: 3px;
132
- }
133
- }
134
-
135
- /* ==========================================================================
136
- STRUQ MOTION GATE. Do not edit.
137
- With JavaScript on (the `js` class the layout sets before paint) the animated elements start
138
- hidden, so nothing flashes before its entrance. Each recipe marks itself data-ready the moment
139
- it has set its own start state. If the scripts never run, the failsafe shows everything after
140
- 3s, so no region can stay empty.
141
- ========================================================================== */
142
- .js [data-split]:not([data-ready]) {
143
- visibility: hidden;
144
- animation: struq-show 0s 3s forwards;
145
- }
146
- .js [data-reveal]:not([data-ready]) > * {
147
- opacity: 0;
148
- animation: struq-fade 0s 3s forwards;
149
- }
150
- @media (prefers-reduced-motion: no-preference) {
151
- .js [data-frame="mask"]:not([data-ready]) {
152
- clip-path: inset(100% 0 0 0);
153
- animation: struq-open 0s 3s forwards;
154
- }
155
- .js [data-zoom-frame] {
156
- clip-path: inset(22% 30% 22% 30%);
157
- }
158
- }
159
- /* The cinematic showpieces start in their first key frame, so nothing flashes before it. */
160
- @media (min-width: 768px) and (prefers-reduced-motion: no-preference) {
161
- .js [data-showpiece="image-to-fullbleed"] [data-frame] {
162
- clip-path: inset(31% 40% 31% 40%);
163
- }
164
- .js [data-showpiece="pixel-resolve"] canvas {
165
- opacity: 1;
166
- background: var(--surface-2);
167
- animation: struq-unveil 0s 4s forwards;
168
- }
169
- }
170
- @keyframes struq-unveil { to { opacity: 0; visibility: hidden; } }
171
- @keyframes struq-show { to { visibility: visible; } }
172
- @keyframes struq-fade { to { opacity: 1; } }
173
- @keyframes struq-open { to { clip-path: inset(0 0 0 0); } }
174
-
175
- /* SplitText masks: room for descenders (g, j, p, y) inside each line's clip. */
176
- .struq-line-mask {
177
- padding-block: 0.06em 0.16em;
178
- margin-block: -0.06em -0.16em;
179
12
  }
180
13
 
181
- /* The custom cursor hides the native one only while it is active. */
182
- html.has-cursor,
183
- html.has-cursor * {
184
- cursor: none;
14
+ /* One fluid root-rem knob: author in rem, breakpoints change layout, never size. */
15
+ html {
16
+ font-size: clamp(1rem, 0.92rem + 0.2vw, 1.125rem);
17
+ }
18
+
19
+ /* STRUQ TOKENS: palette graphite-ivory, type serif-editorial. Written by canon.mjs, the block Struq's lab and struq_richting give. */
20
+ [data-blocks] {
21
+ --surface-0: #F3F0E8;
22
+ --surface-1: #E8E1D5;
23
+ --surface-2: oklch(0.8674 0.0168 81.33);
24
+ --surface-3: oklch(0.8101 0.0154 81.33);
25
+ --ink: #272826;
26
+ --ink-muted: color-mix(in oklab, var(--ink) 66%, var(--surface-0));
27
+ --accent: #995029;
28
+ --accent-ink: #F3F0E8;
29
+ --accent-2: #2B4A56;
30
+ --line: color-mix(in oklab, var(--ink) 13%, transparent);
31
+ --on-photo: #F3F0E8;
32
+ --photo-ground: #272826;
33
+ --inverse: #272826;
34
+ --on-inverse: #F3F0E8;
35
+ --field: #995029;
36
+ --on-field: #F3F0E8;
37
+ color-scheme: light;
38
+ --display-weight: 300;
39
+ --face-display: "Fraunces";
40
+ --face-body: "Urbanist";
41
+ --face-accent: "Fraunces";
185
42
  }
@@ -1,11 +1,10 @@
1
- // app/layout.tsx (Struq recipe)
2
- // Replace SITE_TITLE and SITE_DESCRIPTION, and set lang to the site's language.
3
- // The inline script adds the `js` class before first paint: the motion gate in globals.css keys
4
- // off it, so animated elements never flash before their entrance and stay visible without JS.
1
+ // app/layout.tsx (Struq recipe). Set the title, the description and lang.
2
+ // The inline script puts `sq-js` on <html> before first paint: the kit's motion gate keys off it, so an animated
3
+ // element never flashes before its entrance, shows anyway after 3 s, and a visitor without JavaScript sees the page.
4
+ // data-blocks on <body> is the scope every block and the STRUQ TOKENS block read.
5
5
  import type { Metadata } from "next";
6
- import { Cursor } from "@/components/struq/Cursor";
7
- import { SmoothScroll } from "@/components/struq/SmoothScroll";
8
- import { fontVariables } from "./fonts";
6
+ import { SmoothScroll } from "../components/struq/kit/SmoothScroll";
7
+ import { fontHrefs } from "./fonts";
9
8
  import "./globals.css";
10
9
 
11
10
  export const metadata: Metadata = {
@@ -15,13 +14,16 @@ export const metadata: Metadata = {
15
14
 
16
15
  export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
17
16
  return (
18
- <html lang="nl" className={fontVariables} suppressHydrationWarning>
17
+ <html lang="nl" suppressHydrationWarning>
19
18
  <head>
20
- <script dangerouslySetInnerHTML={{ __html: "document.documentElement.classList.add('js')" }} />
19
+ <script dangerouslySetInnerHTML={{ __html: "document.documentElement.classList.add('sq-js')" }} />
20
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
21
+ {fontHrefs.map((href) => (
22
+ <link key={href} rel="stylesheet" href={href} />
23
+ ))}
21
24
  </head>
22
- <body>
25
+ <body data-blocks="">
23
26
  <SmoothScroll>{children}</SmoothScroll>
24
- <Cursor />
25
27
  </body>
26
28
  </html>
27
29
  );
@@ -0,0 +1,77 @@
1
+ # Craft: what makes the blocks feel high end
2
+
3
+ The blocks already do all of this. Read it when you write the glue between blocks, adapt one, or build a section the
4
+ catalogue does not have: your code has to hold the same bar or it shows as the seam.
5
+
6
+ ## Motion
7
+
8
+ - **One clock.** Lenis runs on `gsap.ticker` (`kit/SmoothScroll.tsx`, mounted once in the layout); ScrollTrigger reads
9
+ it. No second `requestAnimationFrame` loop, no `anticipatePin` with smooth scroll. A menu or overlay calls
10
+ `getLenis()?.stop()` and `start()`.
11
+ - **Clip from numbers.** A reveal is a clip on a layer already at its full size, never a scale of the box. Tween plain
12
+ numbers (top, right, bottom, left, radius) and write `clip-path: inset(...)` from them in `onUpdate`; never tween
13
+ one clip-path string into another, which GSAP interpolates through broken intermediate values.
14
+ - **Whole pixels.** Round every computed translate, clip edge and fitted size to whole pixels (`Math.round`, or
15
+ `gsap.utils.snap(1)`), so type and hairlines never sit on half pixels and shimmer while they move.
16
+ - **Decode before show.** `await img.decode()` (with a catch) before a photo is revealed, cut to or swapped in; a
17
+ sequence of photos starts only when all are decoded. A reveal over an undecoded image shows a blank frame.
18
+ - **Scroll-tied and reversing.** A scroll effect scrubs (`scrub: true` or a short number) and runs backwards when the
19
+ visitor scrolls back: scrolling up re-sets the large wordmark, closes the print, lowers the line. A one-way
20
+ entrance on scroll reads as a template. Entrances on load play once.
21
+ - **Hold on the key frames.** A pinned stretch spends part of its length still on each composed frame. One idea per
22
+ screen, long eases (`expo.out` in, `power4.inOut` for curtains), nothing bounces.
23
+ - **Every entrance moves and fades,** never opacity alone. Lines rise through masks (`SplitText` with `mask: "lines"`,
24
+ the `.struq-line-mask` padding keeps descenders).
25
+ - **One handover signal.** Preloaders and page transitions close a cover (`data-curtain="closed"` on `<html>`) and fire
26
+ `curtain:lift` when it opens; a hero runs its entrance through `whenUncovered` from
27
+ `preloaders/preloader-slices-uncovered.ts`, so the same entrance serves the first load and every navigation.
28
+
29
+ ## The gate
30
+
31
+ - `<html class="sq-js">` is set by an inline script before first paint (the layout recipe). The kit's CSS hides only
32
+ under `.sq-js`: `[data-split]`, `[data-reveal]` children and `[data-frame="mask"]` wait for `data-ready`, and a CSS
33
+ animation shows them anyway after 3 s. A visitor without JavaScript, or a script that throws, still gets the page.
34
+ - Never hide content with an inline `opacity: 0` or a class your script removes. Use the gate attributes, or your own
35
+ rule under `.sq-js` with the same 3 s fallback.
36
+ - Under `prefers-reduced-motion` every block lands on a composed still: the final frame, no pin, no scrub. Phones get
37
+ the same still where a pin would fight the thumb.
38
+
39
+ ## Photos and type on photos
40
+
41
+ - **The calm side is measured.** `kit/photo-read.ts` reads the photo's luminance under the type's box; the heroes put
42
+ the headline on the side that needs the weaker veil. The beeldbank's `negativeSpace` sets the first paint and
43
+ breaks a tie; pass `side` only when you know better.
44
+ - **A light veil.** Type over a photo gets the weakest flat veil that gives the headline 3:1 and a running line 4.5:1,
45
+ measured from that photo, capped at about a quarter. Never a gradient scrim, never a dark band by habit.
46
+ `--photo-ground` is the palette's own dark ground for a bar or band laid over a photo.
47
+ - **Never an empty frame.** Every image slot holds a real photo from `lib/struq-images.ts` or the client. A photo shows
48
+ the world around the client, never their own room, team or product unless it is theirs; captions come from the alt.
49
+
50
+ ## Type
51
+
52
+ - **Light display weights.** The display face runs at the pairing's weight (`--display-weight`, 300 for most, 400 where
53
+ the face starts at 400, 700 or 800 only for the poster pairings). Never `font-bold` on a heading; hierarchy comes
54
+ from scale: `text-display`, `text-h2`, `text-h3`, `text-lead`, `text-body`, `text-small`.
55
+ - **Real faces, measured.** A line fitted to the width (a wordmark, a poster line) is measured after
56
+ `document.fonts.ready` with the loaded face, and refitted on `loadingdone`. Never fit with the fallback face, and
57
+ never ask Google for a weight the family does not ship: `canon.mjs type` requests each face's real range.
58
+ - **Word spacing at poster size:** 0.06 to 0.1em on display lines, so tight tracking never runs two words into one.
59
+ - One italic accent face at most, on one word of the page.
60
+ - One fluid root-rem knob (`html { font-size: clamp(...) }` in globals.css): author in rem; breakpoints change the
61
+ layout, never the size.
62
+
63
+ ## Colour and surface
64
+
65
+ - Tokens only: `--surface-0` to `--surface-3`, `--ink`, `--ink-muted`, `--accent`, `--accent-ink`, `--line`,
66
+ `--on-photo`, `--photo-ground`, `--inverse`, `--on-inverse`, `--field`, `--on-field`. The surface steps hold the
67
+ ground's hue; mixes are `in oklab`, never `in oklch` (an oklch mix walks round the hue wheel and turns cream lilac).
68
+ - **One contrast scene,** at most two: wrap a section in `scene-inverse` (the palette's dark ground, light type) or
69
+ `scene-field` (the accent at full strength); every block inside follows the turned tokens.
70
+ - Elevation is a surface step plus a hairline, never a shadow. Radius 0 to 4px. Flat fills, no gradients, no glass.
71
+
72
+ ## Composition
73
+
74
+ - The display type is the composition, not a heading above it: 12 to 20vw on the poster lines, small body beside it.
75
+ - Vary the sections: after a split comes a full-bleed band or hairline rows, never three equal cards. Section padding
76
+ varies by role; one section should feel too empty by template standards.
77
+ - Never two pinned pieces back to back: a plain section breathes between them.