@struq/skill 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -36
- package/package.json +4 -2
- package/skills/struq/SKILL.md +139 -307
- package/skills/struq/blocks/CATALOG.md +426 -0
- package/skills/struq/blocks/backgrounds/background-aurora.tsx +818 -0
- package/skills/struq/blocks/backgrounds/background-facets.tsx +1283 -0
- package/skills/struq/blocks/backgrounds/background-silk.tsx +906 -0
- package/skills/struq/blocks/catalog.json +1302 -0
- package/skills/struq/blocks/footers/footer-contact.tsx +789 -0
- package/skills/struq/blocks/footers/footer-reveal.tsx +313 -0
- package/skills/struq/blocks/headers/header-bar.tsx +652 -0
- package/skills/struq/blocks/headers/menu-curtain.tsx +790 -0
- package/skills/struq/blocks/heroes/hero-framed.tsx +627 -0
- package/skills/struq/blocks/heroes/hero-index.tsx +855 -0
- package/skills/struq/blocks/heroes/hero-poster.tsx +1086 -0
- package/skills/struq/blocks/heroes/hero-split.tsx +685 -0
- package/skills/struq/blocks/heroes/hero-still.tsx +862 -0
- package/skills/struq/{recipes/components/struq → blocks/kit}/Cursor.tsx +1 -1
- package/skills/struq/{recipes/components/struq → blocks/kit}/HeroCanvas.tsx +4 -4
- package/skills/struq/{recipes/components/struq → blocks/kit}/HorizontalGallery.tsx +2 -2
- package/skills/struq/{recipes/components/struq → blocks/kit}/MagneticButton.tsx +1 -1
- package/skills/struq/{recipes/components/struq → blocks/kit}/MediaFrame.tsx +2 -2
- package/skills/struq/{recipes/components/struq → blocks/kit}/PinnedZoom.tsx +14 -4
- package/skills/struq/{recipes/components/struq → blocks/kit}/Reveal.tsx +1 -1
- package/skills/struq/{recipes/components/struq → blocks/kit}/SmoothScroll.tsx +1 -1
- package/skills/struq/{recipes/components/struq → blocks/kit}/SplitHeadline.tsx +1 -1
- package/skills/struq/blocks/kit/blocks.css +2102 -0
- package/skills/struq/blocks/kit/images.ts +17 -0
- package/skills/struq/blocks/kit/photo-read.ts +102 -0
- package/skills/struq/blocks/page-transitions/page-transition-gate.tsx +706 -0
- package/skills/struq/blocks/page-transitions/page-transition-slices.tsx +469 -0
- package/skills/struq/blocks/page-transitions/page-transition-stack.tsx +583 -0
- package/skills/struq/blocks/preloaders/preloader-gate.tsx +358 -0
- package/skills/struq/blocks/preloaders/preloader-slices-uncovered.ts +50 -0
- package/skills/struq/blocks/preloaders/preloader-slices.tsx +289 -0
- package/skills/struq/blocks/preloaders/preloader-stack.tsx +367 -0
- package/skills/struq/blocks/preloaders/preloader-wordmark.tsx +568 -0
- package/skills/struq/blocks/sections/about-studio.tsx +436 -0
- package/skills/struq/blocks/sections/approach.tsx +661 -0
- package/skills/struq/blocks/sections/contact-form.tsx +664 -0
- package/skills/struq/blocks/sections/cta-closing.tsx +763 -0
- package/skills/struq/blocks/sections/fanned-cards.tsx +547 -0
- package/skills/struq/blocks/sections/film-strip.tsx +385 -0
- package/skills/struq/blocks/sections/grid-assembly.tsx +616 -0
- package/skills/struq/blocks/sections/hover-list.tsx +467 -0
- package/skills/struq/blocks/sections/image-trail.tsx +432 -0
- package/skills/struq/blocks/sections/manifesto.tsx +691 -0
- package/skills/struq/blocks/sections/panel-accordion.tsx +380 -0
- package/skills/struq/blocks/sections/reveals.tsx +416 -0
- package/skills/struq/blocks/sections/split-pair.tsx +670 -0
- package/skills/struq/blocks/sections/sticky-chapters.tsx +256 -0
- package/skills/struq/blocks/sections/velocity-type.tsx +269 -0
- package/skills/struq/{cinematic/showpieces/toonstuk-fill-rows.tsx → blocks/showpieces/fill-rows.tsx} +1 -1
- package/skills/struq/{cinematic/showpieces/toonstuk-hero-opener.tsx → blocks/showpieces/hero-opener.tsx} +5 -5
- package/skills/struq/{cinematic/showpieces/toonstuk-image-to-fullbleed.tsx → blocks/showpieces/image-to-fullbleed.tsx} +16 -5
- package/skills/struq/blocks/showpieces/manifesto-scrub.tsx +48 -0
- package/skills/struq/{cinematic/showpieces/toonstuk-panel-wipe.tsx → blocks/showpieces/panel-wipe.tsx} +2 -2
- package/skills/struq/{cinematic/showpieces/toonstuk-pixel-resolve.tsx → blocks/showpieces/pixel-resolve.tsx} +1 -1
- package/skills/struq/{cinematic/showpieces/toonstuk-project-stack.tsx → blocks/showpieces/project-stack.tsx} +2 -2
- package/skills/struq/blocks/showpieces/scrub-connector.tsx +307 -0
- package/skills/struq/{cinematic/showpieces/toonstuk-wordmark-finale.tsx → blocks/showpieces/wordmark-finale.tsx} +1 -1
- package/skills/struq/blocks/transitions/zoom-through.tsx +613 -0
- package/skills/struq/canon/canon.json +627 -0
- package/skills/struq/cinematic/README.md +7 -18
- package/skills/struq/cinematic/examples/code/nextjs/app/components/LenisRoot.tsx +1 -1
- package/skills/struq/cinematic/examples/code/shared/styles.css +1 -1
- package/skills/struq/cinematic/examples/code/vite/src/LenisRoot.tsx +1 -1
- package/skills/struq/cinematic/references/01-core-mental-model.md +1 -1
- package/skills/struq/cinematic/references/02-vocabulary.md +1 -1
- package/skills/struq/cinematic/references/07-stack-integration.md +2 -2
- package/skills/struq/recipes/app/fonts.ts +12 -22
- package/skills/struq/recipes/app/globals.css +34 -177
- package/skills/struq/recipes/app/layout.tsx +13 -11
- package/skills/struq/references/craft.md +77 -0
- package/skills/struq/references/iconography.md +28 -230
- package/skills/struq/references/stack-targets.md +23 -74
- package/skills/struq/references/tells.md +52 -0
- package/skills/struq/references/triage.md +6 -7
- package/skills/struq/scripts/blocks.mjs +156 -0
- package/skills/struq/scripts/canon.mjs +106 -281
- package/skills/struq/scripts/check.mjs +92 -126
- package/skills/struq/scripts/images.mjs +2 -2
- package/skills/struq/scripts/log.mjs +6 -23
- package/skills/struq/scripts/tokens.mjs +222 -0
- package/skills/struq/cinematic/showpieces/README.md +0 -18
- package/skills/struq/cinematic/showpieces/toonstuk-manifesto-scrub.tsx +0 -81
- package/skills/struq/cinematic/showpieces/toonstuk-scrub-connector.tsx +0 -78
- package/skills/struq/references/anti-patterns.md +0 -330
- package/skills/struq/references/baseline-prompt.md +0 -122
- package/skills/struq/references/brand.md +0 -143
- package/skills/struq/references/brief-router.md +0 -218
- package/skills/struq/references/composition.md +0 -190
- package/skills/struq/references/depth-parallax.md +0 -235
- package/skills/struq/references/direction.md +0 -188
- package/skills/struq/references/feel-layer.md +0 -107
- package/skills/struq/references/gsap-api.md +0 -293
- package/skills/struq/references/hero-scenes.md +0 -107
- package/skills/struq/references/imagery.md +0 -125
- package/skills/struq/references/loaders.md +0 -287
- package/skills/struq/references/micro-interactions.md +0 -270
- package/skills/struq/references/mobile-composition.md +0 -70
- package/skills/struq/references/nav-header.md +0 -296
- package/skills/struq/references/niche-packets.md +0 -338
- package/skills/struq/references/page-architecture.md +0 -134
- package/skills/struq/references/page-entrances.md +0 -198
- package/skills/struq/references/performance.md +0 -235
- package/skills/struq/references/pinning-scrub.md +0 -340
- package/skills/struq/references/pitfalls.md +0 -290
- package/skills/struq/references/route-transitions.md +0 -281
- package/skills/struq/references/scaling.md +0 -151
- package/skills/struq/references/scroll-reveals.md +0 -279
- package/skills/struq/references/sections.md +0 -141
- package/skills/struq/references/seo.md +0 -219
- package/skills/struq/references/setup.md +0 -397
- package/skills/struq/references/showcase.md +0 -87
- package/skills/struq/references/struq-imagery.md +0 -107
- package/skills/struq/references/svg-motion.md +0 -390
- package/skills/struq/references/transitions.md +0 -160
- package/skills/struq/references/typography.md +0 -720
- package/skills/struq/references/vibe-presets.md +0 -287
- package/skills/struq/references/webgl-r3f.md +0 -274
- package/skills/struq/references/webgl-scenes.md +0 -593
- package/skills/struq/scripts/font-names.mjs +0 -40
- package/skills/struq/scripts/recipes.mjs +0 -135
- /package/skills/struq/{recipes/lib → blocks/kit}/gsap.ts +0 -0
- /package/skills/struq/{recipes/components/struq → blocks/kit}/scenes/contours.ts +0 -0
- /package/skills/struq/{recipes/components/struq → blocks/kit}/scenes/dots.ts +0 -0
- /package/skills/struq/{recipes/components/struq → blocks/kit}/scenes/ripple.ts +0 -0
- /package/skills/struq/{recipes/components/struq → blocks/kit}/scenes/types.ts +0 -0
|
@@ -13,6 +13,8 @@ import { join, relative, resolve, sep } from "node:path";
|
|
|
13
13
|
import { appendLog } from "./log.mjs";
|
|
14
14
|
import { altFitsBrief, packSignalForBrief } from "./packs.mjs";
|
|
15
15
|
|
|
16
|
+
const CANON = JSON.parse(readFileSync(new URL("../canon/canon.json", import.meta.url), "utf8"));
|
|
17
|
+
|
|
16
18
|
const args = process.argv.slice(2);
|
|
17
19
|
const dirFlag = args.indexOf("--dir");
|
|
18
20
|
const root = resolve(dirFlag >= 0 ? args[dirFlag + 1] : process.cwd());
|
|
@@ -43,7 +45,15 @@ function walk(dir, acc = []) {
|
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
const rel = (p) => relative(root, p).split(sep).join("/");
|
|
46
|
-
|
|
48
|
+
// A shipped file is one the skill installs under components/struq/ at the same path as in its blocks/ copy (or the
|
|
49
|
+
// images module images.mjs writes); anything else there, a scene or section you added, counts as your own.
|
|
50
|
+
const SHIPPED = new URL("../blocks/", import.meta.url);
|
|
51
|
+
const isRecipe = (p) => {
|
|
52
|
+
const r = rel(p);
|
|
53
|
+
if (/(^|\/)lib\/struq-images\.ts$/.test(r)) return true;
|
|
54
|
+
const m = r.match(/(?:^|\/)components\/struq\/(.+)$/);
|
|
55
|
+
return Boolean(m && existsSync(new URL(m[1], SHIPPED)));
|
|
56
|
+
};
|
|
47
57
|
const files = walk(root)
|
|
48
58
|
.filter((p) => !/^(scripts|test|tests|e2e)\//.test(rel(p)))
|
|
49
59
|
.map((p) => ({ path: p, rel: rel(p), recipe: isRecipe(p), text: readFileSync(p, "utf8") }));
|
|
@@ -195,12 +205,13 @@ const plan = read("STRUQ.md");
|
|
|
195
205
|
|
|
196
206
|
/* ------------------------------------------------------------------- TELLS */
|
|
197
207
|
// The combined tell list: Struq's tells (StruqVisual lib/consistentie/tells.ts), the deck-slop-audit
|
|
198
|
-
// patterns and Roy's own rules, as one list. references/
|
|
208
|
+
// patterns and Roy's own rules, as one list. references/tells.md has the why and the fix for
|
|
199
209
|
// each; this is its executable form. FAIL blocks done; WARN asks for a look.
|
|
200
210
|
{
|
|
201
211
|
const G = "TELLS";
|
|
202
212
|
const tell = (name, pattern, fix, opts = {}) => {
|
|
203
|
-
|
|
213
|
+
// Your own files only: the blocks under components/struq/ passed Struq's gate as they are.
|
|
214
|
+
const found = hits(pattern, { scope: own, ...opts });
|
|
204
215
|
if (found.length) add(G, opts.warn ? "WARN" : "FAIL", `${name}: ${fix}`, found);
|
|
205
216
|
else if (!opts.quietPass) add(G, "PASS", `No ${name.toLowerCase()}`);
|
|
206
217
|
};
|
|
@@ -265,6 +276,7 @@ const plan = read("STRUQ.md");
|
|
|
265
276
|
tell("Live dots", /animate-(ping|pulse)/, "no pulsing dot, above all not in a pill.");
|
|
266
277
|
tell("Blue and purple utilities", /\b(text|bg|border|fill|stroke|ring|outline|decoration|shadow|accent)-(blue|indigo|violet|purple|fuchsia|sky)-\d/,
|
|
267
278
|
"the palette tokens carry every colour; blue and purple only when the brand is that, declared in STRUQ.md.");
|
|
279
|
+
tell("Glow", /shadow-\[0_0_|drop-shadow-\[0_0_|(box|text)-shadow:\s*0(px)?\s+0(px)?\s+\d+px|\bblur-(2xl|3xl)\b/, "no coloured light behind a card or a word; elevation is a surface step.");
|
|
268
280
|
tell("Drop-shadow elevation", /\bshadow-(md|lg|xl|2xl)\b/, "elevation is a tonal surface step plus a hairline.");
|
|
269
281
|
tell("Side-stripe borders", /\bborder-[lr]-([2-9]|\[[2-9])/, "no coloured stripe down a card edge; a hairline or nothing.");
|
|
270
282
|
|
|
@@ -328,85 +340,69 @@ const plan = read("STRUQ.md");
|
|
|
328
340
|
}
|
|
329
341
|
|
|
330
342
|
/* ------------------------------------------------------------------ MOTION */
|
|
343
|
+
// The blocks (components/struq/<kind>/) carry their own motion; this checks what the site around them owes them.
|
|
344
|
+
const BLOCK_IMPORT = /from\s*["'][^"']*components\/struq\/(preloaders|page-transitions|headers|heroes|backgrounds|showpieces|transitions|sections|footers)\/([\w-]+)["']/g;
|
|
345
|
+
const usedBlocks = new Set(own.flatMap((f) => [...f.text.matchAll(BLOCK_IMPORT)].map((m) => `${m[1]}/${m[2]}`)));
|
|
346
|
+
const usesKind = (kind) => [...usedBlocks].some((b) => b.startsWith(`${kind}/`));
|
|
331
347
|
{
|
|
332
348
|
const G = "MOTION";
|
|
333
349
|
const all = files.map((f) => f.text).join("\n");
|
|
334
|
-
const missing = ["gsap", "lenis"].filter((d) => !deps[d]);
|
|
335
|
-
|
|
350
|
+
const missing = ["gsap", "@gsap/react", "lenis"].filter((d) => !deps[d]);
|
|
351
|
+
const needsThree = usedBlocks.has("showpieces/hero-opener") || own.some((f) => /from\s*["']three["']|HeroCanvas/.test(f.text));
|
|
352
|
+
if (needsThree && !deps.three) missing.push("three");
|
|
336
353
|
if (missing.length) add(G, "FAIL", `Missing packages: npm install ${missing.join(" ")}`);
|
|
337
|
-
else add(G, "PASS", "
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
-
const
|
|
341
|
-
if (lenis &&
|
|
342
|
-
else add(G, "FAIL", lenis ? "Lenis is not mounted: wrap {children} in <SmoothScroll> in app/layout.tsx." : "No Lenis on gsap.ticker: run
|
|
343
|
-
|
|
344
|
-
if (
|
|
345
|
-
else add(G, "FAIL", "The
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const masks = uses(/<MediaFrame\b|<ImageToFullbleed\b|<PanelWipe\b|<ProjectStack\b|clipPath|clip-path:\s*inset/);
|
|
350
|
-
if (reveals >= 2 && masks) add(G, "PASS", `Masked image reveals and ${reveals} staggered text reveals`);
|
|
351
|
-
else add(G, "FAIL", `Scroll reveals below the floor (${reveals} <Reveal>, masked images: ${masks ? "yes" : "no"}). Wrap text groups in <Reveal> and every image in <MediaFrame>.`);
|
|
352
|
-
|
|
353
|
-
if (uses(/<HorizontalGallery\b|<PinnedZoom\b|<ImageToFullbleed\b|<PanelWipe\b|<ScrubConnector\b|<ProjectStack\b|<ManifestoScrub\b/) || own.some((f) => /pin:\s*(true|[a-zA-Z"'])/.test(f.text) && /scrub/.test(f.text))) add(G, "PASS", "A pinned or horizontal showpiece");
|
|
354
|
-
else add(G, "FAIL", "No showpiece: place the lead toonstuk from the SKILL.md picker (or HorizontalGallery or PinnedZoom) at the act break.");
|
|
355
|
-
|
|
356
|
-
const ownGl = own.filter((f) => /WebGLRenderer|<Canvas\b/.test(f.text));
|
|
357
|
-
if (uses(/<HeroCanvas\b|<HeroOpener\b/)) add(G, "PASS", "WebGL hero layer (HeroCanvas: poster, DPR cap, reduced motion, pause offscreen)");
|
|
358
|
-
else if (ownGl.length) {
|
|
359
|
-
const t = ownGl.map((f) => f.text).join("\n");
|
|
360
|
-
const gaps = [];
|
|
361
|
-
if (!/setPixelRatio\(\s*Math\.min|dpr=\{?\[/.test(t)) gaps.push("a DPR cap (Math.min(devicePixelRatio, 1.75))");
|
|
362
|
-
if (!/prefers-reduced-motion/.test(t)) gaps.push("a reduced-motion still frame");
|
|
363
|
-
if (!/poster|<Image\b|<img\b|fallback/i.test(t)) gaps.push("a poster behind the canvas");
|
|
364
|
-
if (gaps.length) add(G, "FAIL", `The custom WebGL layer is missing ${gaps.join(", ")}. Or build it inside HeroCanvas as a scene.`, ownGl.map((f) => f.rel));
|
|
365
|
-
else add(G, "PASS", "Custom WebGL hero layer with poster, DPR cap and reduced motion");
|
|
366
|
-
} else add(G, "FAIL", "No WebGL hero layer: put <HeroCanvas scene=\"contours\" /> (or ripple, dots, or the MCP background) behind the hero.");
|
|
354
|
+
else add(G, "PASS", "The block packages are installed");
|
|
355
|
+
|
|
356
|
+
const layout = files.find((f) => /(^|\/)app\/layout\.(tsx|jsx)$/.test(f.rel))?.text ?? "";
|
|
357
|
+
const lenis = /new Lenis\(|ReactLenis/.test(all) && /gsap\.ticker\.add/.test(all);
|
|
358
|
+
if (lenis && /<SmoothScroll|<ReactLenis/.test(layout)) add(G, "PASS", "Lenis on the GSAP ticker, mounted in the layout");
|
|
359
|
+
else add(G, "FAIL", lenis ? "Lenis is not mounted: wrap {children} in <SmoothScroll> in app/layout.tsx." : "No Lenis on gsap.ticker: run blocks.mjs init and mount <SmoothScroll> in app/layout.tsx.");
|
|
360
|
+
|
|
361
|
+
if (/classList\.add\(\s*["']sq-js["']\s*\)/.test(layout) && /data-blocks/.test(layout)) add(G, "PASS", "The motion gate: sq-js before paint, data-blocks on <body>");
|
|
362
|
+
else add(G, "FAIL", "The motion gate is missing: the layout puts sq-js on <html> in an inline script before paint and data-blocks on <body> (recipes/app/layout.tsx). Without them the blocks lose their tokens and their first frame.");
|
|
363
|
+
|
|
364
|
+
if (usedBlocks.size) add(G, "PASS", `${usedBlocks.size} Struq block(s) on the page: ${[...usedBlocks].join(", ")}`);
|
|
365
|
+
else add(G, "FAIL", "No Struq block on the page: pick the hero, the sections and the footer from blocks/CATALOG.md (or struq_blokken) and install them with blocks.mjs add.");
|
|
367
366
|
|
|
368
367
|
if (/prefers-reduced-motion/.test(all)) add(G, "PASS", "prefers-reduced-motion respected");
|
|
369
368
|
else add(G, "FAIL", "Nothing reads prefers-reduced-motion.");
|
|
370
|
-
|
|
371
|
-
if (uses(/<MagneticButton\b|<Cursor\b|quickTo/)) add(G, "PASS", "Pointer layer (magnetic CTA or cursor)");
|
|
372
|
-
else add(G, "WARN", "No magnetic CTA or custom cursor. Use <MagneticButton> for the primary CTA unless the genre is a utility.");
|
|
373
369
|
}
|
|
374
370
|
|
|
375
371
|
/* -------------------------------------------------------------------- TYPE */
|
|
376
372
|
{
|
|
377
373
|
const G = "TYPE";
|
|
378
374
|
const fams = new Set();
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
375
|
+
let italicAccent = false;
|
|
376
|
+
for (const f of own) {
|
|
377
|
+
for (const m of f.text.matchAll(/fonts\.googleapis\.com\/css2\?family=([A-Za-z0-9+]+)(:[^&"'\s]*)?/g)) {
|
|
378
|
+
fams.add(m[1].replace(/\+/g, " "));
|
|
379
|
+
if (/ital@1|ital,wght@1/.test(m[2] ?? "")) italicAccent = true;
|
|
382
380
|
}
|
|
383
|
-
|
|
384
|
-
|
|
381
|
+
for (const m of f.text.matchAll(/api\.fontshare\.com\/v2\/css\?f\[\]=([a-z0-9-]+)/g)) fams.add(m[1]);
|
|
382
|
+
for (const m of f.text.matchAll(/import\s*\{([^}]+)\}\s*from\s*["']next\/font\/google["']/g)) {
|
|
383
|
+
m[1].split(",").map((s) => s.trim()).filter(Boolean).forEach((s) => fams.add(s.replace(/_/g, " ")));
|
|
385
384
|
}
|
|
385
|
+
if (/from\s*["']next\/font\/local["']/.test(f.text)) for (const m of f.text.matchAll(/(\w+)\s*=\s*localFont\(/g)) fams.add(`local:${m[1]}`);
|
|
386
386
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
if (fams.size ===
|
|
390
|
-
else
|
|
391
|
-
else if (fams.size === 1) add(G, "WARN", `One face (${[...fams][0]}). Two faces (display and text) unless STRUQ.md declares a one-family system.`);
|
|
392
|
-
else if (fams.size === 0) add(G, "FAIL", "No web fonts loaded: set the pairing in app/fonts.ts with next/font.");
|
|
393
|
-
else add(G, "FAIL", `${fams.size} faces loaded (${[...fams].join(", ")}). Exactly two: one display, one text.`);
|
|
387
|
+
if (fams.size === 2 || (fams.size === 3 && italicAccent)) add(G, "PASS", `Faces: ${[...fams].join(" + ")}`);
|
|
388
|
+
else if (fams.size === 1) add(G, "WARN", `One face (${[...fams][0]}). A display and a text face, unless the pairing is one family.`);
|
|
389
|
+
else if (fams.size === 0) add(G, "FAIL", "No web fonts loaded: canon.mjs type <pairing> writes app/fonts.ts, and the layout links each URL.");
|
|
390
|
+
else add(G, "FAIL", `${fams.size} faces loaded (${[...fams].join(", ")}). One display, one text, at most one italic accent.`);
|
|
394
391
|
|
|
395
392
|
const css = files.filter((f) => f.rel.endsWith(".css")).map((f) => f.text).join("\n");
|
|
396
|
-
const
|
|
393
|
+
const ownCss = own.filter((f) => f.rel.endsWith(".css")).map((f) => f.text).join("\n");
|
|
394
|
+
const weight = (ownCss.match(/--display-weight:\s*(\d{3})/) ?? css.match(/--display-weight:\s*(\d{3})/))?.[1];
|
|
395
|
+
const typeId = plan.match(/^Type:\s*([\w-]+)/m)?.[1];
|
|
396
|
+
const poster = CANON.pairings.find((t) => t.id === typeId && String(t.displayScale?.weight) === weight && Number(weight) > 400);
|
|
397
397
|
if (weight && Number(weight) <= 400) add(G, "PASS", `Display runs light (${weight})`);
|
|
398
|
-
else if (
|
|
399
|
-
else if (
|
|
400
|
-
else add(G, "WARN", "No
|
|
398
|
+
else if (poster) add(G, "PASS", `Display at ${poster.id}'s poster weight (${weight})`);
|
|
399
|
+
else if (weight) add(G, "FAIL", `Display weight ${weight}: run it light (300, or 400 for a face that starts there); heavier only at a poster pairing's own weight.`);
|
|
400
|
+
else add(G, "WARN", "No --display-weight found: the STRUQ TOKENS block sets it (canon.mjs type).");
|
|
401
401
|
|
|
402
402
|
if (/html\s*\{[^}]*font-size:\s*clamp\(/.test(css)) add(G, "PASS", "One fluid root-rem knob");
|
|
403
403
|
else add(G, "FAIL", "No fluid root font size: html { font-size: clamp(1rem, 0.92rem + 0.2vw, 1.125rem); }");
|
|
404
404
|
|
|
405
|
-
|
|
406
|
-
else add(G, "FAIL", "The headline does not use the display role: font-display text-display.");
|
|
407
|
-
|
|
408
|
-
// The hero opener sets the page's h1 itself.
|
|
409
|
-
const h1 = own.reduce((n, f) => n + (f.text.match(/<h1\b|<HeroOpener\b|<SplitHeadline(?![^>]*as=["'](h2|h3|p)["'])[^>]*>/g) ?? []).length, 0);
|
|
405
|
+
const h1 = files.reduce((n, f) => n + (f.text.match(/<h1\b|<SplitHeadline(?![^>]*as=["'](h2|h3|p)["'])[^>]*>/g) ?? []).length, 0);
|
|
410
406
|
if (h1 >= 1) add(G, "PASS", "The page has its h1");
|
|
411
407
|
else add(G, "FAIL", "No h1 on the page.");
|
|
412
408
|
}
|
|
@@ -419,97 +415,67 @@ const plan = read("STRUQ.md");
|
|
|
419
415
|
const open = plan.split(/\r?\n/).filter((l) => /^\s*[-*]\s*\[\s\]/.test(l) && !/check\.mjs/.test(l));
|
|
420
416
|
const done = plan.split(/\r?\n/).filter((l) => /^\s*[-*]\s*\[[xX]\]/.test(l)).length;
|
|
421
417
|
if (open.length) add(G, "FAIL", `${open.length} Build checklist item(s) not ticked in STRUQ.md. Do them, then tick them.`, open.map((l) => l.trim()));
|
|
422
|
-
else if (done >=
|
|
418
|
+
else if (done >= 7) add(G, "PASS", `Build checklist ticked (${done} items)`);
|
|
423
419
|
else add(G, "FAIL", "STRUQ.md has no Build checklist: copy it from SKILL.md and tick each step as you finish it.");
|
|
424
|
-
const need = ["Palette", "Type", "
|
|
425
|
-
if (need.length) add(G, "FAIL", `STRUQ.md does not record
|
|
426
|
-
else add(G, "PASS", "
|
|
420
|
+
const need = ["Palette", "Type", "Images", "Order"].filter((k) => !new RegExp(`^${k}:\\s*\\S`, "mi").test(plan));
|
|
421
|
+
if (need.length) add(G, "FAIL", `STRUQ.md does not record: ${need.map((k) => `${k}:`).join(", ")} (SKILL.md, the plan).`);
|
|
422
|
+
else add(G, "PASS", "Direction and order recorded (palette, type, images, order)");
|
|
427
423
|
}
|
|
428
424
|
}
|
|
429
425
|
|
|
430
426
|
/* -------------------------------------------------------------------- HERO */
|
|
431
427
|
{
|
|
432
428
|
const G = "HERO";
|
|
433
|
-
|
|
434
|
-
const heroFiles = own.filter((f) => /<HeroCanvas\b|<HeroOpener\b/.test(f.text));
|
|
435
|
-
// The hero is the section around HeroCanvas (or the HeroOpener tag): from the section that opens
|
|
436
|
-
// before it to the first </section> after it. Photos further down the page do not count.
|
|
437
|
-
const heroSlice = (text) => {
|
|
438
|
-
const opener = text.indexOf("<HeroOpener");
|
|
439
|
-
const at = opener >= 0 ? opener : text.indexOf("<HeroCanvas");
|
|
440
|
-
if (opener >= 0) return text.slice(at, text.indexOf("/>", at) + 2 || text.length);
|
|
441
|
-
const open = text.lastIndexOf("<section", at);
|
|
442
|
-
const close = text.indexOf("</section>", at);
|
|
443
|
-
return text.slice(open >= 0 ? open : at, close >= 0 ? close : text.length);
|
|
444
|
-
};
|
|
445
|
-
// With no fitting pack photo (imageFit none) the opener is typographic or WebGL on purpose.
|
|
429
|
+
const heroBlock = usesKind("heroes") || usedBlocks.has("showpieces/hero-opener");
|
|
446
430
|
const noFit = /imageFit[^=]*=\s*"none"/.test(read("lib/struq-images.ts") + read("src/lib/struq-images.ts"));
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
431
|
+
if (heroBlock) {
|
|
432
|
+
add(G, "PASS", "A hero block opens the page");
|
|
433
|
+
const heroUse = own.flatMap((f) => [...f.text.matchAll(/<(Hero[A-Z]\w*)\b[^>]*?\/?>/gs)].map((m) => ({ f, tag: m[0] })));
|
|
434
|
+
const bare = noFit ? [] : heroUse.filter(({ tag }) => /<Hero(Framed|Split|Index|Still|Poster)\b/.test(tag) && !/\bimages?=\{|\bimage=\{/.test(tag));
|
|
435
|
+
if (bare.length) add(G, "FAIL", "A photo hero without its photo: pass image={images.hero[0]} (or the client's photo).", bare.map(({ f }) => f.rel));
|
|
436
|
+
} else {
|
|
437
|
+
// A hand-built hero: a photo in it and a display line at poster size.
|
|
438
|
+
const heroFile = own.find((f) => /<h1\b|<SplitHeadline\b/.test(f.text));
|
|
439
|
+
if (!heroFile) add(G, "FAIL", "No hero: open the page with a hero block (blocks/CATALOG.md, Heroes).");
|
|
440
|
+
else {
|
|
441
|
+
const at = heroFile.text.search(/<h1\b|<SplitHeadline\b/);
|
|
442
|
+
const slice = heroFile.text.slice(Math.max(0, heroFile.text.lastIndexOf("<section", at)), heroFile.text.indexOf("</section>", at) + 1 || undefined);
|
|
443
|
+
if (!noFit && !/images\.hero\b|<MediaFrame\b|\bimage=\{/.test(slice)) add(G, "FAIL", "Hero without a photo: a hand-built hero shows images.hero[0] (or the client's photo). Or use a hero block.", [heroFile.rel]);
|
|
444
|
+
const vw = [...(slice.match(/<(?:h1|SplitHeadline)\b[^>]*>/)?.[0] ?? "").matchAll(/text-\[(\d+(?:\.\d+)?)vw\]/g)].map((m) => Number(m[1]));
|
|
445
|
+
if (!vw.length || Math.max(...vw) < 10) add(G, "FAIL", "Hand-built hero line under 10vw: set it at 12 to 20vw, or use a hero block.", [heroFile.rel]);
|
|
446
|
+
else add(G, "PASS", "The hand-built hero line runs at poster size");
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (usesKind("headers") || own.some((f) => /data-wordmark/.test(f.text))) add(G, "PASS", "A header with the wordmark");
|
|
450
|
+
else add(G, "FAIL", "No header: use header-bar or menu-curtain, or mark your own header's name data-wordmark.");
|
|
451
|
+
if (usesKind("footers") || usedBlocks.has("showpieces/wordmark-finale")) add(G, "PASS", "A designed footer block closes the page");
|
|
452
|
+
else add(G, "WARN", "No footer block: close with footer-reveal, footer-contact or wordmark-finale, or design your own as hard as the hero.");
|
|
453
|
+
|
|
467
454
|
const scenes = own.filter((f) => !f.rel.endsWith(".css")).reduce((n, f) => n + (f.text.match(/\bscene-(inverse|field)\b/g) ?? []).length, 0);
|
|
468
|
-
if (scenes === 0) add(G, "FAIL", "No contrast scene: wrap one section
|
|
455
|
+
if (scenes === 0) add(G, "FAIL", "No contrast scene: wrap one section in scene-inverse or scene-field, so the page does not stay on one ground.");
|
|
469
456
|
else if (scenes > 2) add(G, "FAIL", `${scenes} contrast scenes: one, at most two, or the contrast stops being a moment.`);
|
|
470
457
|
else add(G, "PASS", `${scenes} contrast scene${scenes > 1 ? "s" : ""}`);
|
|
471
|
-
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
if (geometry.length) add(G, "FAIL", "Hand-built product geometry: never model the client's product from primitives. Use a shared hero scene with the product photography (pack vitrine for a launch).", geometry);
|
|
458
|
+
|
|
459
|
+
const geometry = hits(/new\s+THREE\.(Box|Cylinder|Torus|Sphere|Capsule|Cone|Tube|Lathe|Extrude|Tetrahedron|Octahedron|Icosahedron)Geometry\b|<(box|cylinder|torus|sphere|capsule|cone|tube|lathe|extrude)Geometry\b/, { scope: own });
|
|
460
|
+
if (geometry.length) add(G, "FAIL", "Hand-built product geometry: never model the client's product from primitives. Show the product photography.", geometry);
|
|
475
461
|
else add(G, "PASS", "No product modelled from primitives");
|
|
476
462
|
}
|
|
477
463
|
|
|
478
464
|
/* --------------------------------------------------------------------- LOG */
|
|
479
|
-
// Every site keeps STRUQ-LOG.md: an entry for each completed checklist step. Step
|
|
480
|
-
//
|
|
465
|
+
// Every site keeps STRUQ-LOG.md: an entry for each completed checklist step. Step 8 (this check) writes its own entry
|
|
466
|
+
// when this run ends, so it is not required beforehand.
|
|
481
467
|
{
|
|
482
468
|
const G = "LOG";
|
|
483
469
|
const log = read("STRUQ-LOG.md");
|
|
484
470
|
if (!log) add(G, "FAIL", "No STRUQ-LOG.md. Start it at the first step: node <skill-dir>/scripts/log.mjs \"Step 1 Project\" \"<what>\" \"<why>\".");
|
|
485
471
|
else {
|
|
486
|
-
const ticked = (plan.match(/^\s*[-*]\s*\[[xX]\]\s*(\d+)/gm) ?? []).map((l) => Number(l.match(/(\d+)\s*$/)[1])).filter((n) => n !==
|
|
472
|
+
const ticked = (plan.match(/^\s*[-*]\s*\[[xX]\]\s*(\d+)/gm) ?? []).map((l) => Number(l.match(/(\d+)\s*$/)[1])).filter((n) => n !== 8);
|
|
487
473
|
const missing = ticked.filter((n) => !new RegExp(`^## Step ${n}\\b`, "m").test(log));
|
|
488
474
|
const entries = (log.match(/^## /gm) ?? []).length;
|
|
489
475
|
const approval = log.match(/^## Step 2 Approval[^\n]*\n([\s\S]*?)(?=^## |(?![\s\S]))/m);
|
|
490
|
-
if (!approval || !/^Reply:\s*["
|
|
476
|
+
if (!approval || !/^Reply:\s*["“‘'].+["”’']\s*$/m.test(approval[1])) {
|
|
491
477
|
add(G, "FAIL", 'No Step 2 Approval entry with the user\'s own reply. The build request is not the approval: stop once, ask "Akkoord? Heb je een logo, foto\'s of echte cijfers, stuur ze nu.", then log the answer word for word: node <skill-dir>/scripts/log.mjs "Step 2 Approval" "Asked: Akkoord? ..." "Reply: \"<their words>\"".');
|
|
492
478
|
} else add(G, "PASS", "The approval stop is logged with the user's reply");
|
|
493
|
-
// The MCP build line (struq_bouwplan, logged with log.mjs mcp): every arc row gets its
|
|
494
|
-
// struq_sectiespec, and the one approval stop sits after the bouwplan, before the projectschil.
|
|
495
|
-
const at = (re) => {
|
|
496
|
-
const m = re.exec(log);
|
|
497
|
-
return m ? m.index : -1;
|
|
498
|
-
};
|
|
499
|
-
const planAt = at(/^## Step \d+ MCP struq_bouwplan\b/m);
|
|
500
|
-
if (planAt >= 0) {
|
|
501
|
-
const planEntry = log.slice(planAt).split(/\n(?=## )/)[0];
|
|
502
|
-
const rows = Number(planEntry.match(/^Arc rows:\s*(\d+)/m)?.[1] ?? 0);
|
|
503
|
-
const specRows = new Set([...log.matchAll(/^## Step \d+ MCP struq_sectiespec\b[^\n]*\n(?:(?!## )[^\n]*\n)*?Row:\s*(\d+)/gm)].map((m) => m[1]));
|
|
504
|
-
if (rows && specRows.size < rows) {
|
|
505
|
-
add(G, "FAIL", `MCP build line incomplete: ${specRows.size} of ${rows} arc rows have their struq_sectiespec. Call struq_sectiespec for every arc row with the planToken before writing that row, and log it: node <skill-dir>/scripts/log.mjs mcp struq_sectiespec --row <n> --id <arc id>.`);
|
|
506
|
-
} else if (rows) add(G, "PASS", `MCP build line: struq_sectiespec for all ${rows} arc rows`);
|
|
507
|
-
const approvalAt = at(/^## Step 2 Approval\b/m);
|
|
508
|
-
const shellAt = at(/^## Step \d+ MCP struq_projectschil\b/m);
|
|
509
|
-
if (approvalAt >= 0 && (approvalAt < planAt || (shellAt >= 0 && approvalAt > shellAt))) {
|
|
510
|
-
add(G, "FAIL", "MCP approval stop out of place: with the MCP, stop once after struq_bouwplan and before struq_projectschil, with the plan and its needs in that one question.");
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
479
|
if (missing.length) add(G, "FAIL", `STRUQ-LOG.md has no entry for ticked step(s) ${missing.join(", ")}. Add one per step with log.mjs: what was done and why.`);
|
|
514
480
|
else add(G, "PASS", `STRUQ-LOG.md has ${entries} entries, one or more for every ticked step`);
|
|
515
481
|
}
|
|
@@ -534,7 +500,7 @@ const warns = results.filter((r) => r.status === "WARN").length;
|
|
|
534
500
|
if (!args.includes("--no-log")) {
|
|
535
501
|
const names = (status) => results.filter((r) => r.status === status).map((r) => `${r.group} ${r.message.split(":")[0].replace(/[.(].*$/, "").trim()}`);
|
|
536
502
|
const passed = results.filter((r) => r.status === "PASS").length;
|
|
537
|
-
appendLog(root, "Step
|
|
503
|
+
appendLog(root, "Step 8 Check", [
|
|
538
504
|
`check.mjs: RESULT ${fails ? "FAIL" : "PASS"}, ${passed} pass, ${fails} fail, ${warns} warn.`,
|
|
539
505
|
...(fails ? [`Fail: ${names("FAIL").join("; ")}.`] : []),
|
|
540
506
|
...(warns ? [`Warn: ${names("WARN").join("; ")}.`] : []),
|
|
@@ -333,7 +333,7 @@ async function main() {
|
|
|
333
333
|
if (droppedImages.length) console.log(`Left out ${droppedImages.length} photos that do not fit this client (${[...new Set(droppedImages.map((d) => d.reason))].join("; ")}).`);
|
|
334
334
|
console.log("Captions: write them from each photo's alt below. Never name a subject the photo does not show.");
|
|
335
335
|
if (imageFit === "none") {
|
|
336
|
-
console.log("NONE of this pack's photos fits the brief.
|
|
336
|
+
console.log("NONE of this pack's photos fits the brief. Use no photos: a background block (aurora, silk, facets) carries the hero's type.");
|
|
337
337
|
console.log("Say so at the approval stop: \"Ik heb geen foto's van je <product>: stuur ze, dan komen ze in de hero.\"");
|
|
338
338
|
} else if (imageFit === "partial") {
|
|
339
339
|
console.log(`Only ${entries.length} photo(s) fit: use them where their alt fits (the material, the city, the street), and nowhere else.`);
|
|
@@ -345,7 +345,7 @@ async function main() {
|
|
|
345
345
|
if (plates > 0) {
|
|
346
346
|
console.log(`\nWARN ${plates} slot(s) use fallback plates. Run this script again when struq.nl answers.`);
|
|
347
347
|
}
|
|
348
|
-
appendLog(root, "Step
|
|
348
|
+
appendLog(root, "Step 4 Images", [
|
|
349
349
|
`images.mjs: pack ${slug} (${why}), ${real} images downloaded and verified, ${plates} fallback plates.`,
|
|
350
350
|
...(droppedImages.length ? [`Left out ${droppedImages.length} photos that do not fit this client: ${[...new Set(droppedImages.map((d) => d.reason))].join("; ")}.`] : []),
|
|
351
351
|
...(imageFit !== "full" ? [`Image fit: ${imageFit}${imageFit === "none" ? ", the opener is typographic or WebGL, and the approval stop asks for photos" : ""}.`] : []),
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// STRUQ.md is the plan and the contract; STRUQ-LOG.md is what happened, in order.
|
|
4
4
|
//
|
|
5
5
|
// From the shell, at every checklist step:
|
|
6
|
-
// node <skill-dir>/scripts/log.mjs "Step
|
|
6
|
+
// node <skill-dir>/scripts/log.mjs "Step 5 Blocks" "hero-still, variant frame, in app/page.tsx." "Why: one calm photo for an architect."
|
|
7
7
|
// add --dir <project> to run from elsewhere
|
|
8
8
|
//
|
|
9
9
|
// From a script: import { appendLog } from "./log.mjs"; appendLog(root, "Step 5 Images", ["...", "..."]);
|
|
@@ -52,33 +52,16 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur
|
|
|
52
52
|
const i = args.indexOf("--dir");
|
|
53
53
|
const root = resolve(i >= 0 ? args[i + 1] : process.cwd());
|
|
54
54
|
const rest = i >= 0 ? args.filter((_, j) => j !== i && j !== i + 1) : args;
|
|
55
|
-
// MCP calls: node log.mjs mcp <
|
|
55
|
+
// MCP calls: node log.mjs mcp <struq_tool> [--note "<what it returned or what you took from it>"]
|
|
56
56
|
if (rest[0] === "mcp") {
|
|
57
57
|
const tool = rest[1];
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
return k >= 0 ? rest[k + 1] : undefined;
|
|
61
|
-
};
|
|
62
|
-
const STEP = { struq_bouwplan: 2, struq_projectschil: 3, struq_motion: 3, struq_achtergrond: 3, struq_sectiespec: 7 };
|
|
58
|
+
const k = rest.indexOf("--note");
|
|
59
|
+
const STEP = { struq_richting: 2, struq_blokken: 2, struq_blok: 5, struq_controle: 8 };
|
|
63
60
|
if (!tool || !/^struq_/.test(tool)) {
|
|
64
|
-
console.error(
|
|
61
|
+
console.error('Usage: node log.mjs mcp <struq_tool> [--note "<text>"]');
|
|
65
62
|
process.exit(2);
|
|
66
63
|
}
|
|
67
|
-
|
|
68
|
-
console.error("Log the bouwplan with its arc length: --rows <number of arc rows>, so the check can count the sectiespec calls.");
|
|
69
|
-
process.exit(2);
|
|
70
|
-
}
|
|
71
|
-
if (tool === "struq_sectiespec" && !opt("--row")) {
|
|
72
|
-
console.error("Log each sectiespec with the arc row it is for: --row <arc index> --id <arc id>.");
|
|
73
|
-
process.exit(2);
|
|
74
|
-
}
|
|
75
|
-
const lines = [
|
|
76
|
-
opt("--rows") ? `Arc rows: ${opt("--rows")}` : null,
|
|
77
|
-
opt("--row") ? `Row: ${opt("--row")}${opt("--id") ? ` (${opt("--id")})` : ""}` : null,
|
|
78
|
-
opt("--token") ? `Plan token: ${opt("--token")}` : null,
|
|
79
|
-
opt("--note") ?? (tool === "struq_bouwplan" ? "The plan; the one approval stop comes next, before struq_projectschil." : "Called."),
|
|
80
|
-
].filter(Boolean);
|
|
81
|
-
const file = appendLog(root, `Step ${STEP[tool] ?? 7} MCP ${tool}`, lines);
|
|
64
|
+
const file = appendLog(root, `Step ${STEP[tool] ?? 5} MCP ${tool}`, [k >= 0 ? rest[k + 1] : "Called."]);
|
|
82
65
|
console.log(`Logged ${tool} in ${file}`);
|
|
83
66
|
process.exit(0);
|
|
84
67
|
}
|