@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
@@ -1,78 +1,27 @@
1
- # Stack Targets: detect Vite vs Next.js, then adapt
1
+ # Vite and other React stacks
2
2
 
3
- Every other reference file is written for **Next.js App Router** (Struq's default
4
- stack). Most of the doctrine is framework-agnostic, but a handful of primitives are
5
- Next-specific and will break or mislead in a Vite project. **Detect the stack before
6
- writing any wiring code**, then translate the Next-only pieces.
3
+ The blocks are written for Next.js App Router. Only four things tie them to it; everything else (GSAP, Lenis, the
4
+ kit, the CSS, the tokens) is plain React and Tailwind v4. Detect the stack first: `next` in package.json and an `app/`
5
+ folder is Next; `vite.config.*` with `src/main.tsx` is Vite.
7
6
 
8
- ---
7
+ `blocks.mjs` refuses a project without `next`: copy the files by hand instead, keeping the layout of `blocks/` under
8
+ `src/components/struq/` so the relative imports hold (each block's files are listed in `blocks/catalog.json`).
9
9
 
10
- ## 1. Detect first
11
-
12
- | Signal | Stack |
10
+ | Next.js | Vite |
13
11
  |---|---|
14
- | `next.config.{js,ts,mjs}` present, or an `app/` directory with `layout.tsx` | **Next.js App Router**: use the reference files as written |
15
- | `next.config` present but `pages/` not `app/` | **Next Pages Router**: providers wrap in `_app.tsx`; `template.tsx` patterns use route events instead |
16
- | `vite.config.{js,ts}` present | **Vite**: translate per §2 (React assumed; Vue/Svelte share the concepts, different syntax) |
17
- | neither (CRA, Astro island, Webflow, plain Vite-less) | framework-agnostic core only: Lenis/GSAP provider + CSS; skip Next primitives |
18
-
19
- Check `package.json` deps to confirm: `next` vs `vite` + `react-dom`. If a router is
20
- present in Vite, it is almost always `react-router-dom`, and that is what the transition
21
- patterns below assume.
22
-
23
- Then read the installed majors (`vite`, `react`, `tailwindcss`, `gsap`, `lenis`, the router) and record them on the `Stack:` line; the installed version beats this file whenever they disagree ([setup.md](setup.md) §0, Known API shifts). Scaffold an empty folder with `npm create vite@latest`.
24
-
25
- ---
26
-
27
- ## 2. Translation table: Next App Router → Vite
28
-
29
- | Concern | Next App Router | Vite (React SPA) |
30
- |---|---|---|
31
- | **Fonts** | `next/font/google` / `next/font/local` exposing CSS vars | `@fontsource/<face>` (npm) **or** self-hosted `@font-face` in CSS; `<link rel="preload">` the woff2 in `index.html`. Map the same `--font-display-face` / `--font-body-face` vars. (Pairing and weight decisions: [typography.md](typography.md).) |
32
- | **Lazy / SSR-free island** (WebGL) | `next/dynamic(() => import("./X"), { ssr: false })` | `React.lazy(() => import("./X"))` + `<Suspense fallback={poster}>`. A Vite SPA is already client-only, so there is no SSR tree to exclude; the lazy boundary is purely for bundle-splitting. |
33
- | **Client/server boundary** | `"use client"` directive on leaf components | **Drop it.** It is a no-op in Vite; everything is a client component. Remove the directive when porting snippets. |
34
- | **Providers mount point** | `app/layout.tsx` wrapping `{children}` | `src/main.tsx` (or `App.tsx`) wrapping `<App/>`: `<SmoothScrollProvider>`, etc. Same component, different host file. |
35
- | **Per-navigation remount** (page-transition entrance) | `app/template.tsx` (re-mounts each navigation) | a wrapper keyed on `useLocation().pathname`, e.g. `<motion.div key={pathname}>` or an `<AnimatePresence mode="wait">` around the routed outlet. Same effect: fresh mount per route. |
36
- | **Route-change transitions** | `template.tsx` + `PageTransitionProvider` ([route-transitions.md](route-transitions.md)) | `AnimatePresence` driven by the router location; the curtain/overlay logic is identical, only the trigger source changes. |
37
- | **Images** | `next/image` (auto sizing, lazy, AVIF/WebP) | `<img>` with **explicit `width`/`height`** (or aspect-ratio box) + `loading="lazy"` + `decoding="async"`; for responsive sources use `srcset` or `vite-imagetools`. Keep explicit dims for CLS regardless. |
38
- | **Metadata / head** | `metadata` export / `generateMetadata` | `react-helmet-async` or set tags in `index.html`. |
39
- | **Icons** | `@struq/icons` (the default; Phosphor light the fallback) | The same React package and root CSS imports in `src/main.tsx`; non-React or Iconify-based stacks use its `iconify.json` (prefix `struq`) or `sprite.svg` ([iconography.md](iconography.md) §2). |
40
- | **Env / public assets** | `/public`, `process.env.NEXT_PUBLIC_*` | `/public`, `import.meta.env.VITE_*`. |
41
-
42
- ---
43
-
44
- ## 3. What is identical on both (do NOT re-derive)
45
-
46
- These carry over unchanged. Only the *mount file* differs:
47
-
48
- - **The smooth-scroll provider**: Lenis driven by `gsap.ticker` with `autoRaf: false` +
49
- `lagSmoothing(0)` ([setup.md](setup.md) §2). One RAF clock. Identical code; mount it in
50
- `app/layout.tsx` (Next) or `main.tsx` (Vite).
51
- - **The GSAP registry module** (`lib/gsap.ts`), `gsap.defaults`, and **all `useGSAP`
52
- rules**: scope ref, `contextSafe`, `autoAlpha`, animate `from` so the SSR/initial HTML
53
- is the final state ([setup.md](setup.md) §1, §3). The same registry covers the plugins used by [svg-motion.md](svg-motion.md) (DrawSVG, MorphSVG).
54
- - **`gsap.matchMedia` reduced-motion guard** ([setup.md](setup.md) §4).
55
- - **The WebGL island's `frameloop:"never"` + GSAP-ticker bridge** ([webgl-r3f.md](webgl-r3f.md) §2,
56
- scene recipes in [webgl-scenes.md](webgl-scenes.md)). R3F is router-agnostic; the single-clock
57
- wiring is the same. Only the lazy-boundary primitive changes (`next/dynamic` → `React.lazy`, per §2).
58
- - **The scaling knob**: one CSS rule in the global stylesheet ([scaling.md](scaling.md)),
59
- `app/globals.css` vs `src/index.css`.
60
- - **All Iconify usage**, the token/`@theme` system, every ScrollTrigger/scrub/pin
61
- technique (including every recipe in [showcase.md](showcase.md)), and all motion *values* ([feel-layer.md](feel-layer.md)).
62
-
63
- The rule of thumb: **anything that touches the DOM, the GSAP ticker, or CSS is portable;
64
- anything that says `next/...` or `"use client"` needs the §2 translation.**
65
-
66
- ---
67
-
68
- ## 4. Vite gotchas worth flagging
69
-
70
- - **No SSR safety net by default.** Code that reads `window` at module scope is fine in a
71
- Vite SPA (no server render), but keep it in effects anyway for correctness and so the
72
- component stays portable back to Next.
73
- - **`ScrollTrigger.refresh()`** is still needed after route changes and after fonts/images
74
- settle. Wire it to the router's location change (the App Router note in
75
- [setup.md](setup.md) §6 applies, just via `react-router` instead of `template.tsx`).
76
- - **StrictMode double-invoke** in dev re-runs effects twice. `useGSAP` cleanup handles
77
- it, but raw `useEffect` GSAP setup will leak, which is another reason never to write raw
78
- `useEffect` for GSAP ([setup.md](setup.md) §3).
12
+ | `next/image` (`<Image fill sizes priority>`) | `<img>` with `width`, `height`, `loading`, `decoding="async"`, and `object-cover` on a positioned box for `fill`. `getImageProps` (page-transition-stack) becomes the plain `src`. |
13
+ | `next/link` | `<a>`, or your router's `Link`. |
14
+ | `next/navigation` in the page transitions (`usePathname`, `useRouter`, `useSearchParams`) | Your router's location and navigate (React Router `useLocation`, `useNavigate`). The transitions only need the current path and a push. |
15
+ | `"use client"` | Delete it, or leave it: Vite ignores it. |
16
+
17
+ The shell, in Vite terms:
18
+
19
+ - `index.html`: in `<head>`, the gate script `<script>document.documentElement.classList.add('sq-js')</script>` before
20
+ any stylesheet, and one `<link rel="stylesheet">` per URL that `canon.mjs print <palette> <pairing>` prints;
21
+ `<body data-blocks>`.
22
+ - `src/index.css`: `@import "tailwindcss";`, the kit import, the `@theme inline` names and the STRUQ TOKENS block from
23
+ `recipes/app/globals.css` (the block comes from `canon.mjs print`).
24
+ - `src/main.tsx`: wrap the app in `SmoothScroll` from the kit.
25
+
26
+ `check.mjs` reads `app/layout.tsx` for the gate; on Vite it reports that line as missing. Check `index.html` by eye
27
+ and note it under Verify in STRUQ.md.
@@ -0,0 +1,52 @@
1
+ # The tell list
2
+
3
+ One list: Struq's tells, the slop audit and Roy's rules. Each is something a generator does by reflex and a studio
4
+ never does. `check.mjs` detects the marked ones (and `struq_controle` returns the same rules when the MCP is
5
+ connected); the rest are judged by eye. The fix is beside each.
6
+
7
+ ## Type and labels
8
+
9
+ - **Eyebrows:** a small label above a heading ("OUR APPROACH", "PREMIUM SERVICE"), any uppercase tracked label or
10
+ marquee. Fix: the heading carries itself; wayfinding is a hairline, a change of surface step, a jump in scale. A
11
+ caption under or on an image (`<figcaption>` or `data-caption`) is media metadata and allowed. (check)
12
+ - **Numbered steps** (01 / 02 / 03) and section numbers. Fix: hairline rows or a sequence the layout makes obvious. (check)
13
+ - **Mono on marketing:** monospace as a design face. Fix: mono only for literal code. (check)
14
+ - **The generator faces:** Inter, Roboto, Arial, Helvetica, Open Sans, Poppins, Montserrat, Lato, Geist, Space
15
+ Grotesk, and Playfair, Anton, Oswald, Cormorant or Manrope as a display. Fix: a canon pairing. (check)
16
+ - **Heavy display** outside the poster pairings, `font-bold` on headings, raw sizes (`text-5xl`) and breakpoint size
17
+ ladders. Fix: the type roles and `--display-weight`. (check)
18
+
19
+ ## Colour and surface
20
+
21
+ - **Gradients of any kind,** gradient text, glass, tinted translucent fills. Fix: flat tokens; a flat veil over a
22
+ photo is the only translucent layer. (check)
23
+ - **Glow:** coloured shadows, neon edges, blurred light behind a card. Fix: none. (check)
24
+ - **Default purple or blue:** an indigo or violet accent nobody chose. Fix: a canon palette; blue only when the brand
25
+ is blue, declared in STRUQ.md as `Cool hue: declared because <the brand colour>`. (check)
26
+ - **Pills,** above all a pill with a dot; pulsing live dots; pastel status badges. Fix: a text link or a rectangular
27
+ button. (check)
28
+ - **Drop-shadow elevation** and side-stripe borders. Fix: a surface step and a hairline.
29
+
30
+ ## Layout and motion
31
+
32
+ - **Icon grids:** three or six identical cards, each an icon, a heading and two grey lines. Fix: hairline rows,
33
+ `hover-list`, `panel-accordion`, or one statement. (check)
34
+ - **Stat banners** ("20+", "99%", "51+") the brief did not give. Fix: leave them out. (check)
35
+ - Uniform section padding, the same hover on everything, opacity-only entrances, single-layer parallax. Fix: the
36
+ craft rules in [craft.md](craft.md).
37
+ - An empty or placeholder frame, a grey box, "photo to come". Fix: a beeldbank photo. (check)
38
+
39
+ ## Copy and icons
40
+
41
+ - **Em and en dashes** in any form, in copy, comments and alt text. Fix: restructure the sentence: a comma, a colon, a
42
+ full stop. (check)
43
+ - Generic CTAs ("Learn more", "Get started", "Lees meer"), generic headings ("Our Services", "Over ons"), hype words
44
+ ("seamless", "naadloos", "next-level"), lorem, Acme, leftover template text. Fix: specific words from the brief. (check)
45
+ - Emoji and the cliche icons (sparkles, rocket, lightning, bulb, wand). Fix: Struq Icons, functional only. (check)
46
+ - Invented facts: testimonials, client logos, stats, awards, prices, addresses, hours, bios, quotes the brief did not
47
+ give. Fix: leave them out and list them under Assumptions.
48
+
49
+ ## Judged by eye
50
+
51
+ The hero-metric template (big number, small label, three times), category-reflex colours (green for eco, blue for
52
+ finance), a heading the paragraph restates, a page that reads as a SaaS template in a studio's palette.
@@ -37,25 +37,25 @@ change its dress.
37
37
  ## Fair game: fix freely (this is what you were called for)
38
38
 
39
39
  - **Layout / structure / IA**: restructure sections, reorder, break the grid, fix the
40
- symmetric "SaaS stack." Layout is execution, not identity. ([page-architecture.md](page-architecture.md))
40
+ symmetric "SaaS stack." Layout is execution, not identity: swap in blocks where the page is weakest (the hero, the work, the footer first).
41
41
  - **Spacing rhythm**: kill uniform `py-16`, vary padding by section role.
42
42
  - **Type scale + weight**: fix the five-mid-sizes problem, lighten hero weights, enforce
43
- the readable floor and contrast minimums ([anti-patterns.md](anti-patterns.md) §12). If the
43
+ the readable floor and contrast minimums. If the
44
44
  brand already owns its typefaces, keep them and change how they are set, not which they are
45
- ([typography.md](typography.md)).
45
+ (their faces go into --face-display and --face-body).
46
46
  - **Surfaces / elevation**: replace `shadow-lg` cards with the tonal ladder.
47
47
  - **Motion / reveals**: replace opacity-only fades, add transform+stagger, fix easing.
48
48
  - **Scaling**: convert px-everything + per-breakpoint size ladders to the fluid root-rem
49
- system ([scaling.md](scaling.md)).
49
+ system ([craft.md](craft.md)).
50
50
  - **Icon misuse**: remove Sparkles/Rocket/Zap/emoji, colored chips, decorative filler;
51
51
  consolidate to one icon family ([iconography.md](iconography.md)): the brand's own set if it has one, else Struq Icons (`@struq/icons`). A redesign that already uses one coherent non-Lucide family keeps it (preserve by default); one family per site, never mixed.
52
- - **Everything in [anti-patterns.md](anti-patterns.md)**: that gallery is the fix list.
52
+ - **Everything in [tells.md](tells.md)**: that list is the fix list.
53
53
 
54
54
  ---
55
55
 
56
56
  ## Workflow
57
57
 
58
- 1. **Scan** with [anti-patterns.md](anti-patterns.md) and collect every tell.
58
+ 1. **Scan** with check.mjs (and struq_controle when connected) and [tells.md](tells.md); collect every tell.
59
59
  2. **Filter** the findings through the preserve-list above. A "fix" that reassigns brand
60
60
  color, rewrites copy, or replaces a component primitive is dropped (or downgraded to a
61
61
  *suggestion* the user can opt into), not applied.
@@ -68,4 +68,3 @@ change its dress.
68
68
  When unsure whether something is identity or execution, **ask** rather than assume, but
69
69
  default to preserving the three categories above.
70
70
 
71
- If the work started from a brief, [brief-router.md](brief-router.md) still runs, with this file's rules overriding it: identity is preserved and the router only re-decides execution.
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+ // Struq blocks, offline: the skill's own copy of Struq's admitted blocks (blocks/, catalog.json), installed into a
3
+ // Next.js App Router project. The same files the MCP's struq_blok returns.
4
+ //
5
+ // node <skill-dir>/scripts/blocks.mjs list [kind] the catalogue, one line per block with its variants
6
+ // node <skill-dir>/scripts/blocks.mjs init the kit, the app shell (on a starter) and the base packages
7
+ // node <skill-dir>/scripts/blocks.mjs add <slug> [<slug>] a block and every file it imports, plus its packages
8
+ // flags: --dir <project>, --force (replace files you already have), --no-install
9
+ //
10
+ // Everything lands under components/struq/ in the layout of Struq's blocks/ (kit/, heroes/, sections/ ...), so the
11
+ // relative imports between them hold. A layout or stylesheet you wrote yourself is never overwritten.
12
+ import { spawnSync } from "node:child_process";
13
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
14
+ import { dirname, join, relative, resolve } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import { appendLog } from "./log.mjs";
17
+
18
+ const here = dirname(fileURLToPath(import.meta.url));
19
+ const BLOCKS = join(here, "..", "blocks");
20
+ const RECIPES = join(here, "..", "recipes");
21
+ const CATALOG = JSON.parse(readFileSync(join(BLOCKS, "catalog.json"), "utf8")).blocks;
22
+
23
+ const args = process.argv.slice(2);
24
+ const dirAt = args.indexOf("--dir");
25
+ const root = resolve(dirAt >= 0 ? args[dirAt + 1] : process.cwd());
26
+ const force = args.includes("--force");
27
+ const install = !args.includes("--no-install");
28
+ const [cmd, ...rest] = args.filter((x, i) => !x.startsWith("--") && (dirAt < 0 || i !== dirAt + 1));
29
+
30
+ const src = existsSync(join(root, "src", "app")) ? "src" : "";
31
+ const target = (...p) => join(root, src, "components", "struq", ...p);
32
+ const copied = [];
33
+ const kept = [];
34
+ const notes = [];
35
+
36
+ function place(from, to, overwrite = force) {
37
+ if (existsSync(to) && !overwrite) return kept.push(relative(root, to));
38
+ mkdirSync(dirname(to), { recursive: true });
39
+ copyFileSync(from, to);
40
+ copied.push(relative(root, to).split("\\").join("/"));
41
+ }
42
+
43
+ function walk(dir) {
44
+ return readdirSync(dir).flatMap((n) => (statSync(join(dir, n)).isDirectory() ? walk(join(dir, n)) : [join(dir, n)]));
45
+ }
46
+
47
+ function project() {
48
+ const pkgPath = join(root, "package.json");
49
+ if (!existsSync(pkgPath)) {
50
+ console.error(`FAIL no package.json in ${root}. Scaffold first: npx create-next-app@latest . --ts --tailwind --app --eslint --yes`);
51
+ process.exit(2);
52
+ }
53
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
54
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
55
+ if (!deps.next) {
56
+ console.error("FAIL not a Next.js project. The blocks use next/image and next/link; for Vite read references/stack-targets.md.");
57
+ process.exit(2);
58
+ }
59
+ return deps;
60
+ }
61
+
62
+ function installMissing(deps, wanted) {
63
+ const need = [...wanted].filter((d) => !deps[d]);
64
+ const dev = need.filter((d) => d.startsWith("@types/"));
65
+ const runtime = need.filter((d) => !d.startsWith("@types/"));
66
+ for (const [list, flag] of [[runtime, ""], [dev, "-D "]]) {
67
+ if (!list.length) continue;
68
+ const line = `npm install ${flag}${list.join(" ")}`;
69
+ if (!install) {
70
+ notes.push(`Run: ${line}`);
71
+ continue;
72
+ }
73
+ console.log(`> ${line}`);
74
+ if (spawnSync(line, { cwd: root, stdio: "inherit", shell: true }).status !== 0) notes.push(`The install failed. Run it yourself: ${line}`);
75
+ }
76
+ }
77
+
78
+ function report(step, what) {
79
+ console.log(`\nCopied ${copied.length} file(s)${copied.length ? ":" : "."}`);
80
+ for (const f of copied) console.log(` ${f}`);
81
+ if (kept.length) console.log(`Kept ${kept.length} existing file(s) (--force replaces them).`);
82
+ for (const n of notes) console.log(`NOTE ${n}`);
83
+ appendLog(root, step, [what, ...(notes.length ? [`Notes: ${notes.join(" ")}`] : [])]);
84
+ }
85
+
86
+ const commands = {
87
+ list() {
88
+ const kind = rest[0];
89
+ const rows = CATALOG.filter((b) => !kind || b.kind === kind);
90
+ if (!rows.length) throw new Error(`No blocks of kind "${kind}". Kinds: ${[...new Set(CATALOG.map((b) => b.kind))].join(", ")}`);
91
+ for (const b of rows) {
92
+ console.log(`${b.kind.padEnd(16)} ${b.slug.padEnd(24)} ${b.title}${b.variants.length ? ` [${b.variants.map((v) => v.id).join(", ")}]` : ""}`);
93
+ }
94
+ console.log(`\n${rows.length} block(s). What each does and its variants: blocks/CATALOG.md.`);
95
+ },
96
+
97
+ init() {
98
+ const deps = project();
99
+ // The kit, minus the WebGL hero layer: HeroCanvas and its scenes need three, so they come with hero-opener.
100
+ for (const f of walk(join(BLOCKS, "kit"))) {
101
+ const r = relative(join(BLOCKS, "kit"), f).split("\\").join("/");
102
+ if (r === "HeroCanvas.tsx" || r.startsWith("scenes/")) continue;
103
+ place(f, target("kit", r));
104
+ }
105
+ const appDir = join(root, src, "app");
106
+ const read = (p) => (existsSync(p) ? readFileSync(p, "utf8") : "");
107
+ const layout = read(join(appDir, "layout.tsx"));
108
+ if (!layout || /Create Next App|Geist/.test(layout) || force) place(join(RECIPES, "app", "layout.tsx"), join(appDir, "layout.tsx"), true);
109
+ else if (!/data-blocks/.test(layout)) {
110
+ notes.push("app/layout.tsx is your own: add the sq-js script, the font links, data-blocks on <body> and <SmoothScroll> (recipes/app/layout.tsx).");
111
+ }
112
+ const css = read(join(appDir, "globals.css"));
113
+ if (!css || (!css.includes("STRUQ TOKENS") && /--background|--foreground|Arial|prefers-color-scheme/.test(css)) || force) {
114
+ place(join(RECIPES, "app", "globals.css"), join(appDir, "globals.css"), true);
115
+ } else if (!css.includes("STRUQ TOKENS")) notes.push("app/globals.css is your own: add the kit import and the STRUQ TOKENS block from recipes/app/globals.css.");
116
+ place(join(RECIPES, "app", "fonts.ts"), join(appDir, "fonts.ts"));
117
+ for (const starter of ["next.svg", "vercel.svg", "file.svg", "globe.svg", "window.svg"]) rmSync(join(root, "public", starter), { force: true });
118
+ installMissing(deps, ["gsap", "@gsap/react", "lenis"]);
119
+ report("Step 1 Project", `blocks.mjs init copied the kit and ${copied.length} file(s) in all, kept ${kept.length}.`);
120
+ console.log("OK init ready");
121
+ },
122
+
123
+ add() {
124
+ if (!rest.length) throw new Error("Usage: blocks.mjs add <slug> [<slug> ...]. The slugs: blocks.mjs list");
125
+ const deps = project();
126
+ const wanted = new Set();
127
+ const added = [];
128
+ for (const slug of rest) {
129
+ const b = CATALOG.find((x) => x.slug === slug);
130
+ if (!b) throw new Error(`Unknown block "${slug}". Run: blocks.mjs list`);
131
+ for (const f of b.files) place(join(BLOCKS, f), target(f));
132
+ for (const p of b.packages) wanted.add(p);
133
+ added.push(b);
134
+ }
135
+ installMissing(deps, wanted);
136
+ for (const b of added) {
137
+ const from = `@/components/struq/${b.file.replace(/\.tsx?$/, "")}`;
138
+ console.log(`\n${b.slug}: import { ${b.exports.join(", ")} } from "${from}";`);
139
+ if (b.variants.length) console.log(` variant: ${b.variants.map((v) => `"${v.id}"`).join(" | ")} (default "${b.variants[0].id}")`);
140
+ }
141
+ report("Step 5 Blocks", `blocks.mjs added ${added.map((b) => b.slug).join(", ")}.`);
142
+ console.log("OK blocks added");
143
+ },
144
+ };
145
+
146
+ const run = commands[cmd];
147
+ if (!run) {
148
+ console.error("Usage: blocks.mjs list [kind] | init | add <slug> [<slug> ...] [--dir <project>] [--force] [--no-install]");
149
+ process.exit(2);
150
+ }
151
+ try {
152
+ run();
153
+ } catch (e) {
154
+ console.error(`FAIL ${e.message}`);
155
+ process.exitCode = 1;
156
+ }
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+ // Struq canon, offline: Struq's palettes and type pairings, written into the project as the one token block the
3
+ // blocks read. The same block, derivation and font requests as Struq's block lab and the MCP's struq_richting
4
+ // (scripts/tokens.mjs), from the snapshot in canon/canon.json that the skill's release copies from Struq.
5
+ //
6
+ // node <skill-dir>/scripts/canon.mjs list every palette and pairing
7
+ // node <skill-dir>/scripts/canon.mjs palette <id> writes the STRUQ TOKENS block in app/globals.css
8
+ // node <skill-dir>/scripts/canon.mjs type <id> the same block with the pairing, and app/fonts.ts
9
+ // node <skill-dir>/scripts/canon.mjs print <palette> <pairing> prints the block and the font URLs, writes nothing
10
+ // add --dir <project> to run from elsewhere
11
+ //
12
+ // Node 18 or later, no network, no dependencies.
13
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
14
+ import { dirname, join, resolve } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import { appendLog } from "./log.mjs";
17
+ import { fontHrefs, headFamily, lightness, paletteTokens } from "./tokens.mjs";
18
+
19
+ const here = dirname(fileURLToPath(import.meta.url));
20
+ const CANON = JSON.parse(readFileSync(join(here, "..", "canon", "canon.json"), "utf8"));
21
+ const args = process.argv.slice(2);
22
+ const dirAt = args.indexOf("--dir");
23
+ const root = resolve(dirAt >= 0 ? args[dirAt + 1] : process.cwd());
24
+ const [cmd, a, b] = args.filter((_, i) => dirAt < 0 || (i !== dirAt && i !== dirAt + 1));
25
+
26
+ const palette = (id) => {
27
+ const p = CANON.palettes.find((x) => x.id === id);
28
+ if (!p) throw new Error(`Unknown palette "${id}". Run: canon.mjs list`);
29
+ return p;
30
+ };
31
+ const pairing = (id) => {
32
+ const t = CANON.pairings.find((x) => x.id === id);
33
+ if (!t) throw new Error(`Unknown pairing "${id}". Run: canon.mjs list`);
34
+ return t;
35
+ };
36
+ const faces = (t) => [t.display, t.body, t.accentItalic].filter(Boolean).map(headFamily);
37
+
38
+ export function tokenBlock(p, t) {
39
+ const lines = paletteTokens(p, t).map(([k, v]) => ` ${k}: ${v};`);
40
+ return `/* STRUQ TOKENS: palette ${p.id}, type ${t.id}. Written by canon.mjs, the block Struq's lab and struq_richting give. */\n[data-blocks] {\n${lines.join("\n")}\n}`;
41
+ }
42
+
43
+ const BLOCK = /\/\* STRUQ TOKENS[^*]*\*\/\s*\[data-blocks\]\s*\{[\s\S]*?\n\}/;
44
+
45
+ function cssFile() {
46
+ const path = ["app/globals.css", "src/app/globals.css"].map((f) => join(root, f)).find(existsSync);
47
+ if (!path) throw new Error("No app/globals.css. Run blocks.mjs init first.");
48
+ return path;
49
+ }
50
+
51
+ function write(p, t) {
52
+ const path = cssFile();
53
+ const css = readFileSync(path, "utf8");
54
+ if (!BLOCK.test(css)) throw new Error(`No STRUQ TOKENS block in ${path}. Run blocks.mjs init, or copy the block from recipes/app/globals.css.`);
55
+ writeFileSync(path, css.replace(BLOCK, tokenBlock(p, t)));
56
+ return path;
57
+ }
58
+
59
+ function current() {
60
+ const css = readFileSync(cssFile(), "utf8");
61
+ const m = css.match(/STRUQ TOKENS: palette (\S+), type (\S+?)\./);
62
+ return { palette: m?.[1] ?? CANON.palettes[0].id, type: m?.[2] ?? CANON.pairings[0].id };
63
+ }
64
+
65
+ function writeFonts(t) {
66
+ const hrefs = fontHrefs(t, CANON);
67
+ const file = join(root, existsSync(join(root, "src", "app")) ? "src/app" : "app", "fonts.ts");
68
+ writeFileSync(
69
+ file,
70
+ `// app/fonts.ts: written by the Struq skill (canon.mjs type ${t.id}): ${faces(t).join(", ")}.
71
+ // Each Google face is asked for its real weight range, then as the bare family as a fallback, because Google refuses a
72
+ // request for a weight the family lacks. The layout puts one <link> per URL in <head>; the token block in globals.css
73
+ // names the faces (--face-display, --face-body, --face-accent), so the blocks measure with the real face.
74
+ export const fontHrefs: string[] = ${JSON.stringify(hrefs, null, 2)};
75
+ `,
76
+ );
77
+ return { file, hrefs };
78
+ }
79
+
80
+ const commands = {
81
+ list() {
82
+ console.log(`PALETTES (${CANON.palettes.length}): id, mood, ground. "kept out" means kept for a brand that asks, never picked unasked.`);
83
+ for (const p of CANON.palettes) {
84
+ const ground = lightness(p.colors.background) < 0.5 ? "dark" : "light";
85
+ console.log(` ${p.id.padEnd(40)} ${(p.mood ?? "").padEnd(44)} ${ground}${p.autoPick === false ? " kept out" : ""}`);
86
+ }
87
+ console.log(`\nPAIRINGS (${CANON.pairings.length}): id, display + body (+ italic accent), display weight`);
88
+ for (const t of CANON.pairings) {
89
+ const [d, bo, ac] = [t.display, t.body, t.accentItalic].map((s) => (s ? headFamily(s) : null));
90
+ console.log(` ${t.id.padEnd(26)} ${d} + ${bo}${ac ? ` (+ ${ac} italic)` : ""}, ${t.displayScale?.weight ?? 300}${t.autoPick === false ? " kept out" : ""}`);
91
+ }
92
+ console.log(`\nSnapshot: Struq ${CANON.source.ref} ${CANON.source.commit}.`);
93
+ },
94
+ palette() {
95
+ const p = palette(a);
96
+ const t = pairing(current().type);
97
+ const path = write(p, t);
98
+ console.log(`Wrote palette ${p.id} (${p.mood}) to ${path.replace(root, ".")}, with type ${t.id}.`);
99
+ console.log(`STRUQ.md line: Palette: ${p.id} (${p.mood})`);
100
+ appendLog(root, "Step 3 Canon palette", [`canon.mjs wrote palette ${p.id} (${p.mood}) into the STRUQ TOKENS block.`]);
101
+ console.log("OK palette written");
102
+ },
103
+ type() {
104
+ const t = pairing(a);
105
+ const p = palette(current().palette);
106
+ const path = write(p, t);
107
+ const { file, hrefs } = writeFonts(t);
108
+ console.log(`Wrote type ${t.id} (${faces(t).join(" + ")}) to ${path.replace(root, ".")} and ${file.replace(root, ".")} (${hrefs.length} font URLs).`);
109
+ console.log(`STRUQ.md line: Type: ${t.id}: ${faces(t).join(" + ")}`);
110
+ appendLog(root, "Step 3 Canon type", [`canon.mjs wrote type ${t.id}: ${faces(t).join(" + ")}, display weight ${t.displayScale?.weight ?? 300}.`]);
111
+ console.log("OK type written");
112
+ },
113
+ print() {
114
+ const p = palette(a);
115
+ const t = pairing(b ?? CANON.pairings[0].id);
116
+ console.log(tokenBlock(p, t));
117
+ console.log("");
118
+ for (const h of fontHrefs(t, CANON)) console.log(h);
119
+ },
120
+ };
121
+
122
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
123
+ const run = commands[cmd];
124
+ if (!run || (cmd !== "list" && !a)) {
125
+ console.error("Usage: canon.mjs list | palette <id> | type <id> | print <palette> [pairing] [--dir <project>]");
126
+ process.exit(2);
127
+ }
128
+ try {
129
+ run();
130
+ } catch (e) {
131
+ console.error(`FAIL ${e.message}`);
132
+ process.exitCode = 1;
133
+ }
134
+ }