@struq/skill 0.2.2 → 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 (122) hide show
  1. package/README.md +23 -35
  2. package/package.json +3 -2
  3. package/skills/struq/SKILL.md +148 -212
  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/blocks/kit/Cursor.tsx +117 -0
  19. package/skills/struq/blocks/kit/HeroCanvas.tsx +278 -0
  20. package/skills/struq/blocks/kit/HorizontalGallery.tsx +106 -0
  21. package/skills/struq/blocks/kit/MagneticButton.tsx +92 -0
  22. package/skills/struq/blocks/kit/MediaFrame.tsx +96 -0
  23. package/skills/struq/blocks/kit/PinnedZoom.tsx +89 -0
  24. package/skills/struq/blocks/kit/Reveal.tsx +64 -0
  25. package/skills/struq/blocks/kit/SmoothScroll.tsx +57 -0
  26. package/skills/struq/blocks/kit/SplitHeadline.tsx +68 -0
  27. package/skills/struq/blocks/kit/blocks.css +2102 -0
  28. package/skills/struq/blocks/kit/gsap.ts +21 -0
  29. package/skills/struq/blocks/kit/images.ts +17 -0
  30. package/skills/struq/blocks/kit/photo-read.ts +102 -0
  31. package/skills/struq/blocks/kit/scenes/contours.ts +161 -0
  32. package/skills/struq/blocks/kit/scenes/dots.ts +110 -0
  33. package/skills/struq/blocks/kit/scenes/ripple.ts +99 -0
  34. package/skills/struq/blocks/kit/scenes/types.ts +114 -0
  35. package/skills/struq/blocks/page-transitions/page-transition-gate.tsx +706 -0
  36. package/skills/struq/blocks/page-transitions/page-transition-slices.tsx +469 -0
  37. package/skills/struq/blocks/page-transitions/page-transition-stack.tsx +583 -0
  38. package/skills/struq/blocks/preloaders/preloader-gate.tsx +358 -0
  39. package/skills/struq/blocks/preloaders/preloader-slices-uncovered.ts +50 -0
  40. package/skills/struq/blocks/preloaders/preloader-slices.tsx +289 -0
  41. package/skills/struq/blocks/preloaders/preloader-stack.tsx +367 -0
  42. package/skills/struq/blocks/preloaders/preloader-wordmark.tsx +568 -0
  43. package/skills/struq/blocks/sections/about-studio.tsx +436 -0
  44. package/skills/struq/blocks/sections/approach.tsx +661 -0
  45. package/skills/struq/blocks/sections/contact-form.tsx +664 -0
  46. package/skills/struq/blocks/sections/cta-closing.tsx +763 -0
  47. package/skills/struq/blocks/sections/fanned-cards.tsx +547 -0
  48. package/skills/struq/blocks/sections/film-strip.tsx +385 -0
  49. package/skills/struq/blocks/sections/grid-assembly.tsx +616 -0
  50. package/skills/struq/blocks/sections/hover-list.tsx +467 -0
  51. package/skills/struq/blocks/sections/image-trail.tsx +432 -0
  52. package/skills/struq/blocks/sections/manifesto.tsx +691 -0
  53. package/skills/struq/blocks/sections/panel-accordion.tsx +380 -0
  54. package/skills/struq/blocks/sections/reveals.tsx +416 -0
  55. package/skills/struq/blocks/sections/split-pair.tsx +670 -0
  56. package/skills/struq/blocks/sections/sticky-chapters.tsx +256 -0
  57. package/skills/struq/blocks/sections/velocity-type.tsx +269 -0
  58. package/skills/struq/blocks/showpieces/fill-rows.tsx +74 -0
  59. package/skills/struq/blocks/showpieces/hero-opener.tsx +128 -0
  60. package/skills/struq/blocks/showpieces/image-to-fullbleed.tsx +89 -0
  61. package/skills/struq/blocks/showpieces/manifesto-scrub.tsx +48 -0
  62. package/skills/struq/blocks/showpieces/panel-wipe.tsx +89 -0
  63. package/skills/struq/blocks/showpieces/pixel-resolve.tsx +107 -0
  64. package/skills/struq/blocks/showpieces/project-stack.tsx +86 -0
  65. package/skills/struq/blocks/showpieces/scrub-connector.tsx +307 -0
  66. package/skills/struq/blocks/showpieces/wordmark-finale.tsx +105 -0
  67. package/skills/struq/blocks/transitions/zoom-through.tsx +613 -0
  68. package/skills/struq/canon/canon.json +627 -0
  69. package/skills/struq/cinematic/README.md +7 -18
  70. package/skills/struq/cinematic/examples/code/nextjs/app/components/LenisRoot.tsx +1 -1
  71. package/skills/struq/cinematic/examples/code/shared/styles.css +1 -1
  72. package/skills/struq/cinematic/examples/code/vite/src/LenisRoot.tsx +1 -1
  73. package/skills/struq/cinematic/references/01-core-mental-model.md +1 -1
  74. package/skills/struq/cinematic/references/02-vocabulary.md +1 -1
  75. package/skills/struq/cinematic/references/07-stack-integration.md +2 -2
  76. package/skills/struq/recipes/app/fonts.ts +12 -0
  77. package/skills/struq/recipes/app/globals.css +42 -0
  78. package/skills/struq/recipes/app/layout.tsx +30 -0
  79. package/skills/struq/references/craft.md +77 -0
  80. package/skills/struq/references/iconography.md +28 -230
  81. package/skills/struq/references/stack-targets.md +23 -74
  82. package/skills/struq/references/tells.md +52 -0
  83. package/skills/struq/references/triage.md +6 -7
  84. package/skills/struq/scripts/blocks.mjs +156 -0
  85. package/skills/struq/scripts/canon.mjs +134 -0
  86. package/skills/struq/scripts/check.mjs +513 -0
  87. package/skills/struq/scripts/images.mjs +360 -0
  88. package/skills/struq/scripts/log.mjs +75 -0
  89. package/skills/struq/scripts/packs.mjs +156 -0
  90. package/skills/struq/scripts/tokens.mjs +222 -0
  91. package/skills/struq/references/anti-patterns.md +0 -265
  92. package/skills/struq/references/baseline-prompt.md +0 -118
  93. package/skills/struq/references/brand.md +0 -143
  94. package/skills/struq/references/brief-router.md +0 -219
  95. package/skills/struq/references/composition.md +0 -191
  96. package/skills/struq/references/depth-parallax.md +0 -235
  97. package/skills/struq/references/direction.md +0 -188
  98. package/skills/struq/references/feel-layer.md +0 -107
  99. package/skills/struq/references/gsap-api.md +0 -293
  100. package/skills/struq/references/imagery.md +0 -144
  101. package/skills/struq/references/loaders.md +0 -287
  102. package/skills/struq/references/micro-interactions.md +0 -270
  103. package/skills/struq/references/mobile-composition.md +0 -70
  104. package/skills/struq/references/nav-header.md +0 -296
  105. package/skills/struq/references/page-architecture.md +0 -134
  106. package/skills/struq/references/page-entrances.md +0 -198
  107. package/skills/struq/references/performance.md +0 -235
  108. package/skills/struq/references/pinning-scrub.md +0 -341
  109. package/skills/struq/references/pitfalls.md +0 -290
  110. package/skills/struq/references/route-transitions.md +0 -281
  111. package/skills/struq/references/scaling.md +0 -151
  112. package/skills/struq/references/scroll-reveals.md +0 -280
  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
@@ -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
 
@@ -0,0 +1,12 @@
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
+ ];
@@ -0,0 +1,42 @@
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";
5
+
6
+ /* The theme names the blocks use that the kit leaves to the site. */
7
+ @theme inline {
8
+ --color-accent: var(--accent);
9
+ --color-accent-2: var(--accent-2);
10
+ --color-photo-ground: var(--photo-ground);
11
+ --font-display: var(--face-display), ui-serif, Georgia, serif;
12
+ }
13
+
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";
42
+ }
@@ -0,0 +1,30 @@
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
+ import type { Metadata } from "next";
6
+ import { SmoothScroll } from "../components/struq/kit/SmoothScroll";
7
+ import { fontHrefs } from "./fonts";
8
+ import "./globals.css";
9
+
10
+ export const metadata: Metadata = {
11
+ title: "SITE_TITLE",
12
+ description: "SITE_DESCRIPTION",
13
+ };
14
+
15
+ export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
16
+ return (
17
+ <html lang="nl" suppressHydrationWarning>
18
+ <head>
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
+ ))}
24
+ </head>
25
+ <body data-blocks="">
26
+ <SmoothScroll>{children}</SmoothScroll>
27
+ </body>
28
+ </html>
29
+ );
30
+ }
@@ -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.
@@ -1,255 +1,53 @@
1
- # Iconography: one icon family + craft doctrine
1
+ # Icons
2
2
 
3
- ## Decide here
3
+ Struq Icons is the default: `npm i @struq/icons` (MIT), the Iconify set `struq` for non-React stacks, browsable at
4
+ [struq.nl/iconen](https://struq.nl/iconen). A two-tone set on one 24 grid, the base tone for the shape and the accent
5
+ on the part that carries the meaning, each icon with its own hover motion.
4
6
 
5
- Planning reads only this section ([direction.md](direction.md) §4); the rest is for the build.
7
+ ## Whether an icon earns its place
6
8
 
7
- - **Family, in this order, stop at the first that fits:** (1) the brand's own drawn set; (2) **Struq Icons**, the default (`npm i @struq/icons`, MIT, 182 icons: 118 two-tone with a hover motion, 14 status, 50 glyphs; browse at [struq.nl/iconen](https://struq.nl/iconen)); (3) **Phosphor, weight `light`** (`@phosphor-icons/react`, `thin` for luxury) when the set lacks what the site needs and drawing it is not worth it, or the concept wants a softer, rounder line; (4) **Iconoir or Tabler** at stroke 1.25 when the concept wants a sharper, technical line.
8
- - **One icon family per site, never mixed.** Never two, never Lucide by reflex. If the chosen family lacks an icon, draw it to that family's rules or move the whole site down the list (§2).
9
- - **Struq Icons on a Struq site:** `strokeWidth={1.5}` next to light display type (`1.25` for luxury); the accent mapped to the site's own `--color-accent` (§2), not the set's default flame; `states.css` on, so icons rest in one tone and the accent shows where you act.
10
- - **Size and color:** one size per context (`size={20}` inline, `size={24}` nav, or a `size-5` / `size-6` class so the icon rides the root-rem knob), ink-colored; color only for state and semantics.
11
- - **Budget:** functional icons only (nav, close, chevrons, form states, CTA anchors). No 3-up feature-card icons, no Sparkles, Rocket or Zap, no emoji, no colored chips.
12
- - **Motion:** the set's own hover motion (200ms, hover and focus only, never on load, never looping, off under reduced motion) is inside the budget. Other animated icons under 500ms on click, never looping in standard UI, static under reduced motion.
13
- - **Record in STRUQ.md SYSTEM:** `Icons: Struq Icons, @struq/icons, strokeWidth 1.5, accent = --color-accent, states.css on`.
9
+ Functional only: navigation, close, chevrons, an arrow on a text link, form states. Never a row of feature-card icons,
10
+ never an icon in a coloured chip, never sparkles, rocket, lightning, bulb, wand or emoji. Most studio sections need
11
+ none: the blocks carry their links as type.
14
12
 
15
- ---
16
-
17
-
18
- Two separate decisions, never collapse them:
19
-
20
- 1. **Whether an icon earns its place**: the Struq taste gate. Most marketing
21
- sections need *fewer* icons than the reflex wants; icon-as-filler is an AI tell.
22
- 2. **How the icon behaves once it's in**: the craft doctrine below (family, register,
23
- color, state, animation timing, accessibility).
24
-
25
- The gate (§1) governs *where*; the doctrine (§2 to §8) governs *how*. A perfectly-crafted
26
- icon in the wrong place is still wrong.
27
-
28
- ---
29
-
30
- ## 1. The gate: does it earn its place?
31
-
32
- Keep the existing bans hard:
33
-
34
- - **Never** as 3-up feature-card filler (centered icon + h3 + two gray lines), the single
35
- most recognizable generated section ([anti-patterns.md](anti-patterns.md) §2, §13).
36
- - **Never** Sparkles / Rocket / Zap / lightning, or emoji (🚀✨💡). They are the visual signature
37
- of generated UI.
38
- - **Never** in a colored circle/chip background.
39
- - **Never** five sizes / five strokes scattered across a page.
40
-
41
- Icons **earn their place** when they do real work:
42
-
43
- - **Functional affordances**: nav, search, close, chevrons, form-field states, toggles.
44
- - **Scannability** in menus and dashboards: a per-row icon lets the eye decode the list
45
- faster than text alone (humans parse a glyph in ~1/10s).
46
- - **Button visual-anchors**: an icon on a CTA gives a fast hint of the action.
47
- - **Paragraph / feature separators**: between blocks of a "wall of text," an icon marks a
48
- visual point of separation. It must *reinforce* the message, never *replace* it.
49
-
50
- When in doubt, **no icon**. Hairline rules, ghost type, a single oversized numeral as an
51
- accent, or real imagery often carry decoration better in a marketing context.
52
-
53
- ---
54
-
55
- ## 2. Source: one coherent family per project, chosen in this order
56
-
57
- The family is a SYSTEM decision, recorded in STRUQ.md ([direction.md](direction.md) §4), not
58
- something picked per component. Resolve it top to bottom and stop at the first that fits:
59
-
60
- | # | Family | When | Install | Weight to match light display type |
61
- |---|---|---|---|---|
62
- | 1 | **The brand's own icons** | The client already has a drawn set | their files, inlined as components | as drawn |
63
- | 2 | **Struq Icons** | The default for every Struq build: a two-tone set on one 24 grid (a base tone for the shape, an accent on the part that carries the meaning), each icon with its own hover motion and a draw-in variant | `npm i @struq/icons`; sprite, plain SVG and Iconify JSON (prefix `struq`) for non-React stacks | `strokeWidth={1.5}` (the set's default 2 is Lucide weight; `1.25` for luxury) |
64
- | 3 | **Phosphor** | A softer, rounder line, or a site that needs icons the Struq set does not have | `npm i @phosphor-icons/react`, or Iconify `ph:` | `weight="light"` (or `"thin"` for luxury) |
65
- | 4 | **Iconoir** or **Tabler** | A sharper, more technical or architectural line | `npm i iconoir-react` / `@tabler/icons-react`, or Iconify `iconoir:` / `tabler:` | Tabler `stroke={1.25}`; Iconoir `strokeWidth={1.25}` |
66
-
67
- ### Struq Icons: the wiring
68
-
69
- Import the stylesheets once, at the root, before the site's own stylesheet; then import icons by name. Every icon is a named PascalCase export (`Search`, `ArrowRight`, `External`, `ChevronDown`, `Close`); the barrel tree-shakes. Check a name against the installed `dist/index.d.ts` or [struq.nl/iconen](https://struq.nl/iconen) before using it: the set names icons by meaning, so the up-right arrow is `External`, not `ArrowUpRight`.
13
+ ## Wiring (React)
70
14
 
71
15
  ```tsx
72
- // app/layout.tsx (Next) or src/main.tsx (Vite): once, before globals.css
16
+ // app/layout.tsx, once, before globals.css
73
17
  import "@struq/icons/tokens.css";
74
18
  import "@struq/icons/icons.css";
75
- import "@struq/icons/states.css"; // optional: one tone at rest, the accent on hover, focus and current
19
+ import "@struq/icons/states.css"; // one tone at rest, the accent on hover, focus and current
76
20
  ```
77
21
 
78
22
  ```css
79
- /* globals.css: the set's accent follows the site's palette, not its default flame.
80
- On body, so it wins over tokens.css's :root and .dark rules by inheritance. */
23
+ /* globals.css: the set's accent follows the palette, not its default. On body, so it wins by inheritance. */
81
24
  body {
82
25
  --icon-base: currentColor;
83
- --icon-accent: var(--color-accent);
84
- --icon-accent-live: var(--color-accent);
26
+ --icon-accent: var(--accent);
27
+ --icon-accent-live: var(--accent);
85
28
  }
86
29
  ```
87
30
 
88
31
  ```tsx
89
32
  import { ArrowRight, Search } from "@struq/icons";
90
33
 
91
- <a href="/work" className="font-ui text-ui inline-flex items-center gap-2">
92
- Selected work <ArrowRight size={20} strokeWidth={1.5} />
93
- </a>
94
-
95
- <button type="button" className="grid size-12 place-items-center">
96
- <Search size={24} strokeWidth={1.5} label="Search" /> {/* the only content: give it a label */}
97
- </button>
98
- ```
99
-
100
- - **Props:** `size` (16, 20, 24, 32 or 48 px; a `size-5` class overrides it in rem), `strokeWidth`, `label` (names it for screen readers; without one it is `aria-hidden` decoration), `animate` (`"hover"` default, `"draw"`, `"none"`), `color` and `accentColor` (any CSS colour, set per icon; they beat the variables). Other SVG props pass through.
101
- - **Server components.** The icons use no hooks, so they render in server components. Only the draw-in entry is a client component.
102
- - **Draw-in** for one empty state, first run or hero moment, never a list: `import { StruqIconDraw } from "@struq/icons/draw"`, `<StruqIconDraw icon={Inbox} size={48} delay={0.2} />`. It needs `motion` (already in the stack), draws once, never loops, and shows the finished icon on the server, without JS and under reduced motion.
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
- - **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
- - **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.
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
-
109
- **Never Lucide as the reflex.** It is the default of every generated UI, so it reads as
110
- generated. **Never two families on one site.** If the chosen family is missing an icon, either
111
- draw the missing icon to its rules (its grid, its stroke, its caps and joins) or move the whole
112
- site to the next family. Patching gaps with a second set is the icon version of three display
113
- fonts on one page.
114
-
115
- **Iconify** is an optional transport for families 3 and 4 (and for Struq Icons outside React) when you want any family through one
116
- component: one API over about 200k icons across every major set. It is a delivery method, not a
117
- second family.
118
-
119
- ```bash
120
- # Next / any React # Vite (compile-time, zero runtime, tree-shaken)
121
- npm i @iconify/react npm i -D unplugin-icons @iconify/json
122
- ```
123
-
124
- ```tsx
125
- // @iconify/react: works in both Next and Vite
126
- import { Icon } from "@iconify/react";
127
- <Icon icon="ph:arrow-up-right" className="size-5 text-ink" /> // Phosphor
128
- <Icon icon="iconoir:search" className="size-5 text-ink" /> // Iconoir
129
- ```
130
-
131
- ```ts
132
- // Vite alternative: unplugin-icons, imported as components (no runtime fetch)
133
- import ArrowUpRight from "~icons/ph/arrow-up-right";
134
- <ArrowUpRight className="size-5 text-ink" />
34
+ <a href="/werk">Bekijk het werk <ArrowRight size={20} strokeWidth={1.5} /></a>
35
+ <button type="button"><Search size={24} strokeWidth={1.5} label="Zoeken" /></button>
135
36
  ```
136
37
 
137
- **Pick one family and stay in it.** Consistency in *style, weight, corner treatment, and
138
- fill-vs-stroke* is what makes an icon set read as designed. Lock the chosen family in the
139
- project's icon component so nothing else can be imported by accident.
140
-
141
- ---
142
-
143
- ## 3. Two registers: system vs decorative
144
-
145
- | Register | Use | Traits |
146
- |---|---|---|
147
- | **System** | menus, buttons, inputs, table rows, anything **≤24px** | minimal, reduced to essentials, legible at small size, single stroke |
148
- | **Decorative / expressive** | bigger explanatory moments, feature anchors, animated (Lordicon/Lottie) | richer, more detail, only at **larger** sizes |
149
-
150
- **Complex/decorative icons die at small sizes**: their detail turns to mud below ~24px.
151
- If it's small, it's a system icon. If it's expressive, it's big. Don't mix the registers
152
- within one role.
153
-
154
- One stroke across the set, as the §2 table sets it (Struq Icons `strokeWidth={1.5}`,
155
- Phosphor `weight="light"`, Tabler `stroke={1.25}`, Iconoir `strokeWidth={1.25}`), one size per
156
- context (e.g. `size-5` inline, `size-6` nav), ink-colored by default. Struq Icons' two tones
157
- are not two colours in the §4 sense: at rest under `states.css` they read as one tone, and the
158
- accent is the site's one accent, shown only where the visitor acts.
159
-
160
- ---
161
-
162
- ## 4. Color
163
-
164
- - **One color across the set** by default: ink. An icon set in four hues reads as no
165
- decision made (same rule as the one-accent color system).
166
- - Color earns its place only for **state** (hover/pressed/active) and **semantics**:
167
- red/warning for errors, a success check, etc.
168
- - Never rely on color *alone* to carry meaning (accessibility, see §7).
169
-
170
- ---
171
-
172
- ## 5. States & feedback
173
-
174
- An icon the user can act on must *look* actionable and must confirm the action:
175
-
176
- - Distinct **default / hover / pressed**, via motion, color, or both. A clickable icon
177
- that never changes reads as decoration.
178
- - **Feedback through the icon itself:** a checkmark confirms success, a warning sign flags
179
- an error, a state-swap (empty→full trash) signals what happened. Digital UI lacks the
180
- physical click of a real switch, so this feedback matters *more*, not less.
181
-
182
- ---
183
-
184
- ## 6. Animated icons (Lordicon / Lottie): timing is locked
185
-
186
- Animated icons convey more meaning than static ones and drive engagement, but only with
187
- discipline. The same budget as micro-interactions applies: **2 to 3 per view**, never a field
188
- of looping motion.
189
-
190
- **Timing (hard limits):**
191
-
192
- | Trigger | Duration | Notes |
193
- |---|---|---|
194
- | **Click / tap** | **≤ 500ms** | must keep up with repeat clicks; longer feels out of sync and becomes a distraction by the 3rd press |
195
- | **Hover (decorative)** | **1500-3000ms** | the delight sweet spot; more complex symbol → longer end of the range |
196
- | GIF fallback loop | **2000-5000ms delay between loops** | only if tech forces a looping GIF |
197
-
198
- **Rules:**
199
-
200
- - **No constant loops in dashboards / standard UI.** They become an unwelcome
201
- distraction (especially for attention-sensitive users). Trigger on hover instead.
202
- A looping animation is fine *as a page loader*, not as ambient dashboard decoration.
203
- (This rhymes with the skill-wide "nothing moves that the user didn't cause" principle,
204
- [feel-layer.md](feel-layer.md) §5.)
205
- - **No extreme flashing or flickering**: a seizure/motion-sensitivity hazard. Smooth the
206
- frequency; never strobe.
207
- - **Natural physics only.** Motion that breaks real-world dynamics (unnatural bounce,
208
- impossible 3D, irregular movement) leaves users unsettled and confused. Smooth, weighted,
209
- accurate. Every movement must serve a purpose and match the icon's meaning (a security
210
- icon shouldn't "wave and wiggle").
211
- - **Don't overload.** Too many animated icons, especially on loop, drown the message.
212
-
213
- Animated icons are a micro-interaction: they obey the same library split and gating as
214
- everything in [micro-interactions.md](micro-interactions.md). Lottie/Lordicon players are
215
- state/lifecycle-driven (Motion territory for triggering), hover effects are
216
- `@media (hover: hover)`-gated, and **reduced-motion → static icon** (render the still
217
- frame, never the animation).
218
-
219
- ---
220
-
221
- ## 7. Accessibility
222
-
223
- - **Meaningful icon:** give it an accessible name via `aria-label` (or visible label, §8).
224
- An icon that carries content needs alt text so screen readers convey it.
225
- - **Decorative icon:** `aria-hidden`, which keeps it out of the a11y tree entirely.
226
- - **Never color-alone** for meaning (color-vision-deficient and low-vision users). Pair
227
- with shape, label, or motion.
228
- - **Touch target ≥ 48×48px**: the tappable area can exceed the visual glyph. Keep spacing
229
- between grouped tappable icons so they don't misfire.
230
- - **Reduced motion** → static icon (see §6).
231
-
232
- ---
38
+ - `strokeWidth={1.5}` beside light display type (1.25 for a luxury register); one size per context (20 inline, 24 in
39
+ the bar). `label` names an icon that is a control's only content; without it the icon is decoration.
40
+ - Names are by meaning: the up-right arrow is `External`. Check a name in `dist/index.d.ts` before using it.
41
+ `Link`, `Image`, `File` and `Text` clash with Next: import `LinkIcon`, `ImageIcon`, `FileIcon`, `TextIcon`.
42
+ - The hover motion is the set's own (hover and focus only, never on load, off under reduced motion).
233
43
 
234
- ## 8. Labels, universal icons, conflicts
44
+ ## Without React
235
45
 
236
- - **Labels:** before users click, they need to know what to expect. Universal icons (home,
237
- play, stop, pin, search) are self-evident; anything ambiguous needs a **1 to 3 word label**
238
- using an action word (Buy, Continue, Subscribe). Icons reinforce; labels disambiguate.
239
- - **Universal icons reduce cognitive load.** Lean on the globally-recognized set where it
240
- fits, beyond cultural differences.
241
- - **Conflicting icons** (heart vs star both mean "favorite/save/rate"): don't avoid them,
242
- **consciously pick one** and label its function clearly. Don't use one icon for two
243
- purposes or two icons for one purpose.
244
- - **Cultural context** matters for unique/symbolic icons, so consider the audience.
245
- - **Test** new icons with outsiders for *recognizability* (do they understand it?) and
246
- *memorability* (do they recall it a week later?). Re-design or add a label if not.
46
+ `iconify.json` with the prefix `struq` (`struq:search`, `struq:<name>-draw` for a draw-in), `sprite.svg` with
47
+ `<use href="/sprite.svg#search" />`, or the plain SVG files. Same rules.
247
48
 
248
- ---
49
+ ## The one fallback
249
50
 
250
- **Cheap tell:** Lucide `Sparkles`/`Rocket`/`Zap` in colored circle chips, 3-up, as
251
- feature decoration; an animated icon looping forever in a dashboard; a 500ms+ click
252
- animation that lags the cursor; a tiny decorative icon turned to mud at 16px.
253
- **Instead:** One deliberately chosen icon family, system vs decorative registers kept separate,
254
- ink-colored with color only for state/semantics, animated only where it informs and
255
- within the timing limits, labeled where not universal, `aria` correct, ≥48px targets.
51
+ When the set lacks an icon and drawing it to the set's rules (24 grid, round caps and joins, the site's stroke, base
52
+ and accent as two parts) is not worth it, move the whole site to **Phosphor at `weight="light"`**
53
+ (`@phosphor-icons/react`, or Iconify `ph:`). One family per site, never mixed, never Lucide by reflex.