@struq/skill 0.4.3 → 0.5.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 (50) hide show
  1. package/README.md +1 -1
  2. package/package.json +2 -2
  3. package/skills/struq/SKILL.md +145 -89
  4. package/skills/struq/blocks/CATALOG.md +74 -22
  5. package/skills/struq/blocks/backgrounds/background-aurora.tsx +34 -12
  6. package/skills/struq/blocks/backgrounds/background-facets.tsx +14 -6
  7. package/skills/struq/blocks/backgrounds/background-silk.tsx +40 -15
  8. package/skills/struq/blocks/catalog.json +697 -16
  9. package/skills/struq/blocks/film.json +426 -0
  10. package/skills/struq/blocks/footers/footer-contact.tsx +81 -55
  11. package/skills/struq/blocks/footers/footer-reveal.tsx +1 -1
  12. package/skills/struq/blocks/headers/header-bar.tsx +24 -13
  13. package/skills/struq/blocks/headers/menu-curtain.tsx +24 -11
  14. package/skills/struq/blocks/heroes/hero-framed.tsx +33 -20
  15. package/skills/struq/blocks/heroes/hero-index.tsx +32 -11
  16. package/skills/struq/blocks/heroes/hero-poster.tsx +68 -35
  17. package/skills/struq/blocks/heroes/hero-split.tsx +22 -9
  18. package/skills/struq/blocks/heroes/hero-still.tsx +80 -74
  19. package/skills/struq/blocks/kit/accent.ts +82 -0
  20. package/skills/struq/blocks/kit/blocks.css +78 -35
  21. package/skills/struq/blocks/kit/quiet/Close.tsx +73 -0
  22. package/skills/struq/blocks/kit/quiet/PhotoText.tsx +79 -0
  23. package/skills/struq/blocks/kit/quiet/Rows.tsx +81 -0
  24. package/skills/struq/blocks/kit/quiet/Statement.tsx +157 -0
  25. package/skills/struq/blocks/kit/quiet/index.ts +7 -0
  26. package/skills/struq/blocks/kit/quiet/sheet.ts +12 -0
  27. package/skills/struq/blocks/page-transitions/page-transition-gate.tsx +87 -36
  28. package/skills/struq/blocks/preloaders/preloader-gate.tsx +116 -46
  29. package/skills/struq/blocks/preloaders/preloader-stack.tsx +10 -7
  30. package/skills/struq/blocks/sections/contact-form.tsx +1 -1
  31. package/skills/struq/blocks/sections/cta-closing.tsx +3 -2
  32. package/skills/struq/blocks/sections/fanned-cards.tsx +14 -10
  33. package/skills/struq/blocks/sections/film-strip.tsx +18 -14
  34. package/skills/struq/blocks/sections/grid-assembly.tsx +16 -11
  35. package/skills/struq/blocks/sections/manifesto.tsx +3 -2
  36. package/skills/struq/blocks/sections/panel-accordion.tsx +3 -2
  37. package/skills/struq/blocks/sections/split-pair.tsx +4 -3
  38. package/skills/struq/blocks/showpieces/hero-opener.tsx +76 -17
  39. package/skills/struq/blocks/showpieces/wordmark-finale.tsx +3 -1
  40. package/skills/struq/blocks/transitions/zoom-through.tsx +9 -10
  41. package/skills/struq/canon/canon.json +1 -1
  42. package/skills/struq/references/craft.md +4 -0
  43. package/skills/struq/references/film.md +158 -0
  44. package/skills/struq/references/triage.md +3 -0
  45. package/skills/struq/scripts/blocks.mjs +89 -7
  46. package/skills/struq/scripts/canon.mjs +8 -1
  47. package/skills/struq/scripts/check.mjs +94 -11
  48. package/skills/struq/scripts/film-check.mjs +997 -0
  49. package/skills/struq/scripts/gate.mjs +146 -0
  50. package/skills/struq/scripts/packs.mjs +73 -12
@@ -8,9 +8,13 @@
8
8
  // and one link sit on the calm side, where the surface lies flat and is the page ground itself. The surface lies flat
9
9
  // into the ground over the bottom fifth of the hero, so the hand-over to the next section has no seam.
10
10
  //
11
- // <BackgroundFacets variant="shard" lines={["Ruimte die", "licht vangt"]}
11
+ // <BackgroundFacets variant="shard" lines={["Ruimte die", "licht vangt"]} accent="licht"
12
12
  // lead="Noord ontwerpt woningen en ateliers in Rotterdam." link={{ label: "Bekijk het werk", href: "#werk" }} />
13
13
  //
14
+ // `accent` (optional) is the site's one accent word: where it first stands in the two lines it is set in the pairing's
15
+ // italic accent face, inside its line's mask, and the type's rect is measured again once that face is in. The
16
+ // Statement and the Close carry the same word. Left out, or not in the lines, the hero is exactly as it was.
17
+ //
14
18
  // Variants (different compositions and moves of the same surface, each in light and dark):
15
19
  // shard a few cut shards standing apart in the right third, each a pyramid of unequal planes on its own
16
20
  // footprint, pointing out from the cluster, a light circling them; type on the calm left.
@@ -40,6 +44,7 @@
40
44
  // 2D canvas. No scripts: the composed
41
45
  // hero on the plain ground (blocks.css, "--- background-facets ---", with the 3 s fallback for the lines).
42
46
  import { useEffect, useRef } from "react";
47
+ import { accentFaceReady, accentLines } from "../kit/accent";
43
48
  import { gsap } from "../kit/gsap";
44
49
  import { whenUncovered } from "../preloaders/preloader-slices-uncovered";
45
50
 
@@ -68,6 +73,9 @@ type Props = {
68
73
  variant?: FacetsVariant;
69
74
  /** Two short display lines. */
70
75
  lines: [string, string];
76
+ /** The site's one accent word: where it first stands in the two lines it is set in the pairing's italic accent face
77
+ * (--face-accent), as the Statement and the Close set it. A word the lines do not hold changes nothing. */
78
+ accent?: string;
71
79
  /** One lead sentence. */
72
80
  lead: string;
73
81
  /** One text link. */
@@ -980,7 +988,7 @@ function toRGB(probe: CanvasRenderingContext2D, color: string): RGB {
980
988
  return [r / 255, g / 255, b / 255];
981
989
  }
982
990
 
983
- export function BackgroundFacets({ variant = "shard", lines, lead, link, className = "" }: Props) {
991
+ export function BackgroundFacets({ variant = "shard", lines, accent, lead, link, className = "" }: Props) {
984
992
  const root = useRef<HTMLElement>(null);
985
993
  const canvasRef = useRef<HTMLCanvasElement>(null);
986
994
 
@@ -1216,8 +1224,8 @@ export function BackgroundFacets({ variant = "shard", lines, lead, link, classNa
1216
1224
  const frameEl = el.closest("[data-blocks]");
1217
1225
  if (frameEl) mo.observe(frameEl, { attributes: true, attributeFilter: ["style", "class", "data-theme"] });
1218
1226
  mo.observe(document.documentElement, { attributes: true, attributeFilter: ["class", "data-theme", "style"] });
1219
- // The face arriving late moves the type: measure its rect again.
1220
- document.fonts?.ready.then(() => {
1227
+ // The face arriving late moves the type: measure its rect again (with the accent face in, when a word is marked).
1228
+ document.fonts?.ready.then(() => accentFaceReady(el)).then(() => {
1221
1229
  if (gone) return;
1222
1230
  measure();
1223
1231
  if (!raf) render();
@@ -1248,7 +1256,7 @@ export function BackgroundFacets({ variant = "shard", lines, lead, link, classNa
1248
1256
  }, [variant]);
1249
1257
 
1250
1258
  const mask = (child: React.ReactNode, key?: number) => (
1251
- <span key={key} className="struq-line-mask block overflow-hidden pb-[0.06em]">
1259
+ <span key={key} className="struq-line-mask block">
1252
1260
  <span data-rise="" className="block">
1253
1261
  {child}
1254
1262
  </span>
@@ -1265,7 +1273,7 @@ export function BackgroundFacets({ variant = "shard", lines, lead, link, classNa
1265
1273
  >
1266
1274
  <canvas ref={canvasRef} data-bf-canvas="" aria-hidden="true" className="absolute inset-0 h-full w-full" />
1267
1275
  <div data-copy="" className="absolute">
1268
- <h1 className="m-0 font-display leading-[0.95] tracking-[-0.03em]">{lines.map((line, i) => mask(line, i))}</h1>
1276
+ <h1 className="m-0 font-display leading-[0.95] tracking-[-0.03em]">{accentLines(lines, accent).map((line, i) => mask(line, i))}</h1>
1269
1277
  <div className="mt-6 md:mt-8">
1270
1278
  {/* Full ink: the muted ink measures about 4:1 on the lightest grounds, short of 4.5 for body text. */}
1271
1279
  {mask(<span className="text-body block max-w-[40ch] text-ink">{lead}</span>)}
@@ -7,7 +7,11 @@
7
7
  // real normal and a low light instead of a halftone grid. Raw WebGL (GLSL ES 1.00, so a WebGL1 context runs it too),
8
8
  // no three.js, self-contained.
9
9
  //
10
- // <BackgroundSilk variant="fold" lines={["Zacht licht,", "zware stof."]} lead="..." link={{ href: "#werk", label: "Bekijk het werk" }} />
10
+ // <BackgroundSilk variant="fold" lines={["Zacht licht,", "zware stof."]} accent="licht" lead="..." link={{ href: "#werk", label: "Bekijk het werk" }} />
11
+ //
12
+ // `accent` (optional) is the site's one accent word: where it first stands in the headline it is set in the pairing's
13
+ // italic accent face, inside its line's mask, and the type's box is read once that face is in. The Statement and the
14
+ // Close carry the same word. Left out, or not in the headline, the hero is exactly as it was.
11
15
  //
12
16
  // Three compositions of the same cloth:
13
17
  // - fold: one large diagonal fold through the right half; the type stands on the calm left. The pointer turns the
@@ -24,14 +28,17 @@
24
28
  // sheen. That measure is taken once (and again on a resize or a palette switch), at three moments of the wandering
25
29
  // light, and the cloth eases toward it.
26
30
  //
27
- // Entrance: the canvas is hidden until its first frame is drawn, then fades in while the folds rise from a flat sheet;
28
- // the type rises through masks. Performance: device pixel ratio capped at 1.5, the field drawn at 0.9 of that on a 1x
31
+ // Entrance: the canvas is hidden until its first frame is drawn, then fades in; the folds rise from a flat sheet and the
32
+ // type rises through masks once nothing covers the page (whenUncovered: a preloader or page transition has started to
33
+ // leave, or there is none). Performance: device pixel ratio capped at 1.5, the field drawn at 0.9 of that on a 1x
29
34
  // screen and two thirds on a dense one (half on phones, with fewer noise octaves), paused off screen and in a hidden
30
35
  // tab, everything disposed on
31
36
  // unmount, context loss survived. Reduced motion: one still frame, held. No WebGL: the plain ground with the type.
32
37
  // No scripts: the composed hero on the plain ground (blocks.css, "--- background-silk ---").
33
38
  import { useRef } from "react";
39
+ import { accentFaceReady, accentLines } from "../kit/accent";
34
40
  import { gsap, useGSAP } from "../kit/gsap";
41
+ import { whenUncovered } from "../preloaders/preloader-slices-uncovered";
35
42
 
36
43
  export const SILK_VARIANTS = [
37
44
  { id: "fold", title: "Plooi" },
@@ -45,6 +52,9 @@ type Props = {
45
52
  variant?: SilkVariant;
46
53
  /** The headline, one entry per line. Two short lines read best. */
47
54
  lines: string[];
55
+ /** The site's one accent word: where it first stands in the headline it is set in the pairing's italic accent face
56
+ * (--face-accent), as the Statement and the Close set it. A word the headline does not hold changes nothing. */
57
+ accent?: string;
48
58
  /** One lead sentence under the headline. */
49
59
  lead: string;
50
60
  /** One text link. */
@@ -435,7 +445,9 @@ function silkField(
435
445
 
436
446
  // State the loop owns.
437
447
  let time = reduce ? STILL_T : 0;
438
- let introAt = 0;
448
+ /** When the folds started to rise; -1 while a cover is still over the page, so the sheet waits flat under it. */
449
+ let introAt = -1;
450
+ let stopRise = () => {};
439
451
  let last = 0;
440
452
  let raf = 0;
441
453
  let onScreen = true;
@@ -545,6 +557,7 @@ function silkField(
545
557
 
546
558
  const intro = () => {
547
559
  if (reduce) return 1;
560
+ if (introAt < 0) return 0;
548
561
  const x = Math.min(1, Math.max(0, (time - introAt) / INTRO));
549
562
  return 1 - Math.pow(1 - x, 3);
550
563
  };
@@ -778,13 +791,18 @@ function silkField(
778
791
  document.addEventListener("visibilitychange", onVisibility);
779
792
  resizeObserver.observe(holder);
780
793
  io.observe(el);
781
- document.fonts?.ready.then(onFonts);
782
- introAt = time;
794
+ // The accent face too, when a word is marked: the type's box is read as it will stand.
795
+ document.fonts?.ready.then(() => accentFaceReady(el)).then(onFonts);
796
+ // The folds rise once nothing covers the page: at once, or as a preloader or page transition starts to leave.
797
+ stopRise = whenUncovered(() => {
798
+ introAt = time;
799
+ });
783
800
  if (reduce) still();
784
801
  else kick();
785
802
  },
786
803
  dispose() {
787
804
  disposed = true;
805
+ stopRise();
788
806
  cancelAnimationFrame(raf);
789
807
  cancelAnimationFrame(colourFrame);
790
808
  resizeObserver.disconnect();
@@ -806,7 +824,7 @@ function silkField(
806
824
  };
807
825
  }
808
826
 
809
- export function BackgroundSilk({ variant = "fold", lines, lead, link, className = "" }: Props) {
827
+ export function BackgroundSilk({ variant = "fold", lines, accent, lead, link, className = "" }: Props) {
810
828
  const root = useRef<HTMLElement>(null);
811
829
 
812
830
  useGSAP(
@@ -831,15 +849,21 @@ export function BackgroundSilk({ variant = "fold", lines, lead, link, className
831
849
  gsap.set(rise, { y: 0, yPercent: shown ? 0 : 115 });
832
850
  el.dataset.ready = "";
833
851
 
834
- let played = false;
835
852
  const play = contextSafe(() => {
836
- if (played) return;
837
- played = true;
838
853
  if (!shown) gsap.to(rise, { yPercent: 0, duration: 1.2, ease: "expo.out", stagger: 0.08, delay: 0.25 });
839
854
  });
855
+ // The type rises with the first frame of the cloth, and only once nothing covers the page (a preloader or page
856
+ // transition starting to leave, or none at all).
857
+ let asked = false;
858
+ let stop = () => {};
859
+ const ready = () => {
860
+ if (asked) return;
861
+ asked = true;
862
+ stop = whenUncovered(play);
863
+ };
840
864
  // The type never waits long on a slow shader compile.
841
- const failsafe = window.setTimeout(play, 1400);
842
- const field = silkField(el, holder, variant, reduce, play);
865
+ const failsafe = window.setTimeout(ready, 1400);
866
+ const field = silkField(el, holder, variant, reduce, ready);
843
867
  field.start();
844
868
 
845
869
  // The hand-over: the type leaves first; on fold and ripple the cloth drifts slower than the page, on drape it
@@ -857,6 +881,7 @@ export function BackgroundSilk({ variant = "fold", lines, lead, link, className
857
881
 
858
882
  return () => {
859
883
  window.clearTimeout(failsafe);
884
+ stop();
860
885
  field.dispose();
861
886
  };
862
887
  },
@@ -864,7 +889,7 @@ export function BackgroundSilk({ variant = "fold", lines, lead, link, className
864
889
  );
865
890
 
866
891
  const mask = (child: React.ReactNode, key?: number) => (
867
- <span key={key} className="struq-line-mask block overflow-hidden pb-[0.06em]">
892
+ <span key={key} className="struq-line-mask block">
868
893
  <span data-rise="" className="block">
869
894
  {child}
870
895
  </span>
@@ -884,10 +909,10 @@ export function BackgroundSilk({ variant = "fold", lines, lead, link, className
884
909
  className={`relative h-svh min-h-[34rem] overflow-hidden bg-surface-0 text-ink ${className}`}
885
910
  >
886
911
  <div data-field="" aria-hidden="true" className="absolute inset-0" />
887
- <div className={`relative flex h-full flex-col px-6 pb-[9vh] pt-[12vh] md:px-[5vw] md:pb-[11vh] ${place}`}>
912
+ <div className={`relative flex h-full flex-col px-sheet-margin pb-[9vh] pt-[12vh] md:pb-[11vh] ${place}`}>
888
913
  <div data-copy="" className="max-w-[min(100%,40rem)]">
889
914
  <h1 data-head="" className="font-display text-[clamp(2.75rem,12.5vw,4.5rem)] leading-[0.98] tracking-[-0.03em] md:text-[clamp(3.75rem,6.6vw,7.5rem)]">
890
- {lines.map((line, i) => mask(line, i))}
915
+ {accentLines(lines, accent).map((line, i) => mask(line, i))}
891
916
  </h1>
892
917
  <div data-small="" className="mt-7 md:mt-9">
893
918
  {mask(<span className="block max-w-[34ch] text-[1.0625rem] leading-relaxed text-ink-muted md:text-lg">{lead}</span>)}