@struq/skill 0.4.3 → 0.5.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 +27 -27
- package/cli/struq.mjs +2 -2
- package/package.json +6 -6
- package/skills/struq/SKILL.md +145 -89
- package/skills/struq/blocks/CATALOG.md +74 -22
- package/skills/struq/blocks/backgrounds/background-aurora.tsx +34 -12
- package/skills/struq/blocks/backgrounds/background-facets.tsx +14 -6
- package/skills/struq/blocks/backgrounds/background-silk.tsx +40 -15
- package/skills/struq/blocks/catalog.json +697 -16
- package/skills/struq/blocks/film.json +426 -0
- package/skills/struq/blocks/footers/footer-contact.tsx +81 -55
- package/skills/struq/blocks/footers/footer-reveal.tsx +1 -1
- package/skills/struq/blocks/headers/header-bar.tsx +24 -13
- package/skills/struq/blocks/headers/menu-curtain.tsx +24 -11
- package/skills/struq/blocks/heroes/hero-framed.tsx +33 -20
- package/skills/struq/blocks/heroes/hero-index.tsx +32 -11
- package/skills/struq/blocks/heroes/hero-poster.tsx +68 -35
- package/skills/struq/blocks/heroes/hero-split.tsx +22 -9
- package/skills/struq/blocks/heroes/hero-still.tsx +80 -74
- package/skills/struq/blocks/kit/accent.ts +82 -0
- package/skills/struq/blocks/kit/blocks.css +78 -35
- package/skills/struq/blocks/kit/quiet/Close.tsx +73 -0
- package/skills/struq/blocks/kit/quiet/PhotoText.tsx +79 -0
- package/skills/struq/blocks/kit/quiet/Rows.tsx +81 -0
- package/skills/struq/blocks/kit/quiet/Statement.tsx +157 -0
- package/skills/struq/blocks/kit/quiet/index.ts +7 -0
- package/skills/struq/blocks/kit/quiet/sheet.ts +12 -0
- package/skills/struq/blocks/page-transitions/page-transition-gate.tsx +87 -36
- package/skills/struq/blocks/preloaders/preloader-gate.tsx +116 -46
- package/skills/struq/blocks/preloaders/preloader-stack.tsx +10 -7
- package/skills/struq/blocks/sections/contact-form.tsx +1 -1
- package/skills/struq/blocks/sections/cta-closing.tsx +3 -2
- package/skills/struq/blocks/sections/fanned-cards.tsx +14 -10
- package/skills/struq/blocks/sections/film-strip.tsx +18 -14
- package/skills/struq/blocks/sections/grid-assembly.tsx +16 -11
- package/skills/struq/blocks/sections/manifesto.tsx +3 -2
- package/skills/struq/blocks/sections/panel-accordion.tsx +3 -2
- package/skills/struq/blocks/sections/split-pair.tsx +4 -3
- package/skills/struq/blocks/showpieces/hero-opener.tsx +76 -17
- package/skills/struq/blocks/showpieces/wordmark-finale.tsx +3 -1
- package/skills/struq/blocks/transitions/zoom-through.tsx +9 -10
- package/skills/struq/canon/canon.json +1 -1
- package/skills/struq/references/craft.md +4 -0
- package/skills/struq/references/film.md +158 -0
- package/skills/struq/references/iconography.md +1 -1
- package/skills/struq/references/triage.md +3 -0
- package/skills/struq/scripts/blocks.mjs +89 -7
- package/skills/struq/scripts/canon.mjs +8 -1
- package/skills/struq/scripts/check.mjs +99 -15
- package/skills/struq/scripts/film-check.mjs +997 -0
- package/skills/struq/scripts/gate.mjs +146 -0
- package/skills/struq/scripts/images.mjs +64 -37
- package/skills/struq/scripts/packs.mjs +73 -12
|
@@ -3,17 +3,22 @@
|
|
|
3
3
|
//
|
|
4
4
|
// node <skill-dir>/scripts/check.mjs
|
|
5
5
|
// node <skill-dir>/scripts/check.mjs --dir ./my-site
|
|
6
|
+
// node <skill-dir>/scripts/check.mjs --plan the storyboard in STRUQ.md alone, before anything is built
|
|
6
7
|
//
|
|
7
8
|
// It needs no browser and no dependencies. It reads the source and the public folder and prints
|
|
8
|
-
// PASS, WARN or FAIL per check, in
|
|
9
|
-
//
|
|
10
|
-
// FAIL remains. Fix, run again, and
|
|
9
|
+
// PASS, WARN or FAIL per check, in groups: images, AI tells, the motion floor, the hero, the film
|
|
10
|
+
// (film-check.mjs, the budgets Struq's MCP holds too), typography, plus the plan in STRUQ.md. Every
|
|
11
|
+
// FAIL names the file and line and the fix. Exit code 1 while any FAIL remains. Fix, run again, and
|
|
12
|
+
// only report done on "RESULT: PASS".
|
|
11
13
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
12
14
|
import { join, relative, resolve, sep } from "node:path";
|
|
13
15
|
import { appendLog } from "./log.mjs";
|
|
14
16
|
import { altFitsBrief, packSignalForBrief } from "./packs.mjs";
|
|
17
|
+
import { filmCheck, planCheck } from "./film-check.mjs";
|
|
18
|
+
import { hasApproval } from "./gate.mjs";
|
|
15
19
|
|
|
16
20
|
const CANON = JSON.parse(readFileSync(new URL("../canon/canon.json", import.meta.url), "utf8"));
|
|
21
|
+
const CATALOG = JSON.parse(readFileSync(new URL("../blocks/catalog.json", import.meta.url), "utf8")).blocks;
|
|
17
22
|
|
|
18
23
|
const args = process.argv.slice(2);
|
|
19
24
|
const dirFlag = args.indexOf("--dir");
|
|
@@ -45,6 +50,25 @@ function walk(dir, acc = []) {
|
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
const rel = (p) => relative(root, p).split(sep).join("/");
|
|
53
|
+
|
|
54
|
+
/* -------------------------------------------------------------- PLAN ONLY */
|
|
55
|
+
// --plan holds the storyboard in STRUQ.md to the film budgets before anything is built: no package.json, no build, no
|
|
56
|
+
// checklist and no log needed. The routes are the page files under app/ (at least one).
|
|
57
|
+
if (args.includes("--plan")) {
|
|
58
|
+
const planText = existsSync(join(root, "STRUQ.md")) ? readFileSync(join(root, "STRUQ.md"), "utf8") : "";
|
|
59
|
+
if (!planText) {
|
|
60
|
+
console.log(`FAIL no-plan: No STRUQ.md in ${root}. Fix: write the treatment head and the Storyboard first (SKILL.md step 2).`);
|
|
61
|
+
console.log("PLAN: FAIL");
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
const appDir = ["app", "src/app"].map((d) => join(root, d)).find((d) => existsSync(d));
|
|
65
|
+
const routes = Math.max(1, appDir ? walk(appDir).filter((p) => /(^|[\\/])page\.(tsx|jsx|js)$/.test(p)).length : 0);
|
|
66
|
+
const { findings } = planCheck({ plan: planText, catalog: CATALOG, routes });
|
|
67
|
+
for (const f of findings) console.log(`${f.severity === "fail" ? "FAIL" : "WARN"} ${f.id}: ${f.detail.replace(/\.$/, "")}. Fix: ${f.fix}`);
|
|
68
|
+
const failed = findings.some((f) => f.severity === "fail");
|
|
69
|
+
console.log(`PLAN: ${failed ? "FAIL" : "PASS"}`);
|
|
70
|
+
process.exit(failed ? 1 : 0);
|
|
71
|
+
}
|
|
48
72
|
// A shipped file is one the skill installs under components/struq/ at the same path as in its blocks/ copy (or the
|
|
49
73
|
// images module images.mjs writes); anything else there, a scene or section you added, counts as your own.
|
|
50
74
|
const SHIPPED = new URL("../blocks/", import.meta.url);
|
|
@@ -135,7 +159,7 @@ const plan = read("STRUQ.md");
|
|
|
135
159
|
const pack = mod.match(/imagePack = "([^"]+)"/)?.[1] ?? "?";
|
|
136
160
|
if (imageFit === "none" && !srcs.length) add(G, "PASS", `No photo of pack "${pack}" fits this brief (imageFit none): the opener carries the first screen, and the approval stop asks for photos`);
|
|
137
161
|
else if (missing.length) add(G, "FAIL", `${missing.length} image(s) in ${libFile} are not on disk. Run images.mjs ${pack} again.`, missing);
|
|
138
|
-
else if (plates.length === srcs.length) add(G, "WARN", `
|
|
162
|
+
else if (plates.length === srcs.length) add(G, "WARN", `Struq was unreachable: all ${srcs.length} images are fallback plates. Run images.mjs ${pack} again when online.`);
|
|
139
163
|
else if (plates.length) add(G, "WARN", `${plates.length} fallback plate(s) among the pack images. Run images.mjs ${pack} again when online.`, plates);
|
|
140
164
|
else add(G, "PASS", `${srcs.length} beeldbank images from pack "${pack}", each a verified image file`);
|
|
141
165
|
// An off-topic photo is worse than no photo: a pack the brief does not point at must show
|
|
@@ -176,7 +200,8 @@ const plan = read("STRUQ.md");
|
|
|
176
200
|
const floor = libFile ? Math.min(4, total) : 4;
|
|
177
201
|
if (libFile && imageFit !== "full" && total === 0) add(G, "PASS", "No image placements needed: no pack photo fits");
|
|
178
202
|
else if (used >= floor) add(G, "PASS", `${used} image placements in the page code`);
|
|
179
|
-
|
|
203
|
+
// A floor, not a law: a quiet film may hold fewer photos, so a short count asks for a look rather than blocking.
|
|
204
|
+
else add(G, "WARN", `Only ${used} image placement(s). A Struq site carries a hero image and at least three more: use <MediaFrame image={images.card[0]} />.`);
|
|
180
205
|
// References past what the pack holds render as empty swatches (an inline manifesto image, a card).
|
|
181
206
|
if (libFile) {
|
|
182
207
|
const past = [];
|
|
@@ -209,13 +234,14 @@ const plan = read("STRUQ.md");
|
|
|
209
234
|
{ scope: own, exclude: /placeholder=["'{]/ },
|
|
210
235
|
);
|
|
211
236
|
const emptyFrames = hits(/<(div|figure|span)\b[^>]*(aspect-|aspectRatio)[^>]*(\/>|>\s*<\/(div|figure|span)>)/, { scope: own });
|
|
212
|
-
|
|
237
|
+
// Both domains: struq.app is home, and struq.nl still serves the media for older installs.
|
|
238
|
+
const hotlinks = hits(/https?:\/\/(www\.)?struq\.(app|nl)\/media\//, { scope: own });
|
|
213
239
|
if (placeholders.length || emptyFrames.length) {
|
|
214
240
|
add(G, "FAIL", "Placeholder or empty frames. A Struq site never ships an empty frame: put a MediaFrame with a pack image in each.", [...placeholders, ...emptyFrames]);
|
|
215
241
|
} else add(G, "PASS", "No placeholder or empty image frames");
|
|
216
|
-
if (hotlinks.length) add(G, "FAIL", "Hotlinked
|
|
242
|
+
if (hotlinks.length) add(G, "FAIL", "Hotlinked Struq images: download them with images.mjs and use the local /images/ path.", hotlinks);
|
|
217
243
|
|
|
218
|
-
const credit = uses(/imageCredit|struq\.nl\/beeldbank/);
|
|
244
|
+
const credit = uses(/imageCredit|struq\.app\/photos|struq\.nl\/beeldbank/);
|
|
219
245
|
if (libFile && !credit) add(G, "WARN", "No image credit. Put imageCredit (\"Beeld: Struq\") in the footer as one quiet text-small link.");
|
|
220
246
|
}
|
|
221
247
|
|
|
@@ -278,6 +304,31 @@ const plan = read("STRUQ.md");
|
|
|
278
304
|
}
|
|
279
305
|
}
|
|
280
306
|
if (clipped.length) add(G, "FAIL", "Display heading clipped: a large heading sits in an overflow-hidden box (or on top of an image box) and loses its letters. Give it its own grid cell beside the image, never under or behind it.", clipped);
|
|
307
|
+
|
|
308
|
+
// A heading of your own that cuts its descenders: overflow hidden or clip on the heading itself, or on the element
|
|
309
|
+
// that wraps it directly when that box is held to the line (a fixed height or leading-none). A tall wrapper such as
|
|
310
|
+
// a full-screen hero (h-screen, h-full, h-[100vh]) leaves the heading room and is not counted. Headings the clipped
|
|
311
|
+
// check above already names are left to it, so one heading is reported once.
|
|
312
|
+
const CLIPS = /\boverflow-(?:hidden|clip)\b|overflow:\s*["']?(?:hidden|clip)\b/;
|
|
313
|
+
const HELD = /(?:^|[\s"'`{])(?:h-(?:\d+(?:\.\d+)?|px)|h-\[(?![^\]]*(?:vh|vw|%))[^\]]+\]|leading-none)(?=[\s"'`}]|$)/;
|
|
314
|
+
const TIGHT = /\bleading-\[0?\.[0-8]\d*\]/;
|
|
315
|
+
const named = new Set(clipped.map((c) => c.split(" ")[0]));
|
|
316
|
+
const cut = [];
|
|
317
|
+
const tight = [];
|
|
318
|
+
for (const f of own.filter((x) => !x.rel.endsWith(".css"))) {
|
|
319
|
+
for (const m of f.text.matchAll(/<(h[1-6]|SplitHeadline)\b[^>]*>/g)) {
|
|
320
|
+
const tag = m[0];
|
|
321
|
+
const before = f.text.slice(0, m.index);
|
|
322
|
+
const where = `${f.rel}:${before.split("\n").length}`;
|
|
323
|
+
const open = before.slice(before.lastIndexOf("<")).replace(/\s+$/, "");
|
|
324
|
+
const wrapper = /^<[A-Za-z]/.test(open) && open.endsWith(">") && !open.endsWith("/>") ? open : "";
|
|
325
|
+
const cuts = CLIPS.test(tag) || (wrapper && CLIPS.test(wrapper) && HELD.test(wrapper));
|
|
326
|
+
if (cuts && !named.has(where)) cut.push(`${where} ${(CLIPS.test(tag) ? tag : `${wrapper} ${tag}`).replace(/\s+/g, " ").slice(0, 110)}`);
|
|
327
|
+
if (TIGHT.test(tag)) tight.push(`${where} ${tag.match(TIGHT)[0]}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (cut.length) add(G, "FAIL", "Heading that cuts descenders: overflow-hidden or overflow-clip on a heading of your own (or on a box held to its line by a fixed height or leading-none) cuts g, j, p and y. Drop the clip, and set big type with SplitHeadline or the .struq-line-mask class, whose mask leaves room under the baseline.", cut);
|
|
331
|
+
if (tight.length) add(G, "WARN", "Heading leading under 0.9: descenders of one line run into the next line's capitals. Keep a leading of 1 in a serif, 0.95 in a sans, and look at a line with a g and a j.", tight);
|
|
281
332
|
}
|
|
282
333
|
tell("Numbered sections", />\s*0[1-9]\s*[<./]|['"`]0[1-9]['"`]|`0\$\{/,
|
|
283
334
|
"01, 02, 03 stamped on sections or rows is a tell; keep at most one deliberate oversized numeral.", ownOnly);
|
|
@@ -438,9 +489,23 @@ const usesKind = (kind) => [...usedBlocks].some((b) => b.startsWith(`${kind}/`))
|
|
|
438
489
|
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()));
|
|
439
490
|
else if (done >= 7) add(G, "PASS", `Build checklist ticked (${done} items)`);
|
|
440
491
|
else add(G, "FAIL", "STRUQ.md has no Build checklist: copy it from SKILL.md and tick each step as you finish it.");
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
492
|
+
const has = (k) => new RegExp(`^${k}:\\s*\\S`, "mi").test(plan);
|
|
493
|
+
// The storyboard is the order now; a plan from before the treatment may still carry an Order: line.
|
|
494
|
+
const need = ["Palette", "Type", "Images"].filter((k) => !has(k));
|
|
495
|
+
if (!/^Storyboard:/mi.test(plan) && !has("Order")) need.push("Storyboard");
|
|
496
|
+
if (need.length) add(G, "FAIL", `STRUQ.md does not record: ${need.map((k) => `${k}:`).join(", ")} (SKILL.md, the treatment).`);
|
|
497
|
+
else add(G, "PASS", "Direction and order recorded (palette, type, images, storyboard)");
|
|
498
|
+
|
|
499
|
+
// The treatment: the film the site is cast into (SKILL.md, The film).
|
|
500
|
+
const missing = ["Idea", "Motif", "Lexicon", "Centerpiece"].filter((k) => !has(k));
|
|
501
|
+
const language = plan.match(/^Language:\s*(\S+)/mi)?.[1]?.toLowerCase().replace(/[^a-z]/g, "");
|
|
502
|
+
if (!["weight", "cut", "curtain"].includes(language)) missing.push("Language");
|
|
503
|
+
const board = plan.match(/^Storyboard:[^\n]*\n((?:[ \t]*(?:\d+\.|\.\.\.)[^\n]*\n?)*)/mi);
|
|
504
|
+
const scenes = board ? (board[1].match(/^[ \t]*\d+\.\s+\S/gm) ?? []).length : 0;
|
|
505
|
+
if (!board) missing.push("Storyboard");
|
|
506
|
+
if (missing.length) add(G, "FAIL", `The treatment in STRUQ.md lacks: ${missing.map((k) => `${k}:`).join(", ")}. Write the head of STRUQ.md from the template in SKILL.md step 2 (Language: is weight, cut or curtain; the storyboard numbers its scenes).`);
|
|
507
|
+
else if (scenes < 7 || scenes > 9) add(G, "FAIL", `The storyboard has ${scenes} numbered scene(s): seven to nine, cast from the language's arc (SKILL.md, The film).`);
|
|
508
|
+
else add(G, "PASS", `Treatment recorded: language ${language}, ${scenes} scenes in the storyboard`);
|
|
444
509
|
}
|
|
445
510
|
}
|
|
446
511
|
|
|
@@ -473,7 +538,8 @@ const usesKind = (kind) => [...usedBlocks].some((b) => b.startsWith(`${kind}/`))
|
|
|
473
538
|
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.");
|
|
474
539
|
|
|
475
540
|
const scenes = own.filter((f) => !f.rel.endsWith(".css")).reduce((n, f) => n + (f.text.match(/\bscene-(inverse|field)\b/g) ?? []).length, 0);
|
|
476
|
-
|
|
541
|
+
// The tone arc in the treatment decides where the ground turns; a page without a turn is a choice to look at.
|
|
542
|
+
if (scenes === 0) add(G, "WARN", "No contrast scene: wrap one section in scene-inverse or scene-field, so the page does not stay on one ground.");
|
|
477
543
|
else if (scenes > 2) add(G, "FAIL", `${scenes} contrast scenes: one, at most two, or the contrast stops being a moment.`);
|
|
478
544
|
else add(G, "PASS", `${scenes} contrast scene${scenes > 1 ? "s" : ""}`);
|
|
479
545
|
|
|
@@ -482,6 +548,24 @@ const usesKind = (kind) => [...usedBlocks].some((b) => b.startsWith(`${kind}/`))
|
|
|
482
548
|
else add(G, "PASS", "No product modelled from primitives");
|
|
483
549
|
}
|
|
484
550
|
|
|
551
|
+
/* -------------------------------------------------------------------- FILM */
|
|
552
|
+
// film-check.mjs, Struq's own module: the blocks on the page, in page order, held to the film grammar's budgets (one
|
|
553
|
+
// centerpiece, no pin after it, one language, one clock, one marquee, the pinned screens ...). Your own files only;
|
|
554
|
+
// the copies under components/struq/ are the blocks themselves, not the page.
|
|
555
|
+
{
|
|
556
|
+
const G = "FILM";
|
|
557
|
+
const pageFiles = own
|
|
558
|
+
.filter((f) => !/(^|\/)components\/struq\//.test(f.rel))
|
|
559
|
+
.map((f) => ({ path: f.rel, text: f.text }));
|
|
560
|
+
const { findings, scenes } = filmCheck({ plan, files: pageFiles, catalog: CATALOG });
|
|
561
|
+
for (const f of findings) {
|
|
562
|
+
add(G, f.severity === "fail" ? "FAIL" : "WARN", `${f.id}: ${f.detail.replace(/\.$/, "")}. Fix: ${f.fix}`);
|
|
563
|
+
}
|
|
564
|
+
if (!findings.some((f) => f.severity === "fail")) {
|
|
565
|
+
add(G, "PASS", `The page holds the film budgets (${scenes.length} block${scenes.length === 1 ? "" : "s"} in page order)`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
485
569
|
/* --------------------------------------------------------------------- LOG */
|
|
486
570
|
// Every site keeps STRUQ-LOG.md: an entry for each completed checklist step. Step 8 (this check) writes its own entry
|
|
487
571
|
// when this run ends, so it is not required beforehand.
|
|
@@ -493,8 +577,8 @@ const usesKind = (kind) => [...usedBlocks].some((b) => b.startsWith(`${kind}/`))
|
|
|
493
577
|
const ticked = (plan.match(/^\s*[-*]\s*\[[xX]\]\s*(\d+)/gm) ?? []).map((l) => Number(l.match(/(\d+)\s*$/)[1])).filter((n) => n !== 8);
|
|
494
578
|
const missing = ticked.filter((n) => !new RegExp(`^## Step ${n}\\b`, "m").test(log));
|
|
495
579
|
const entries = (log.match(/^## /gm) ?? []).length;
|
|
496
|
-
|
|
497
|
-
if (!
|
|
580
|
+
// The same test blocks.mjs add and canon.mjs hold before they write a file (gate.mjs).
|
|
581
|
+
if (!hasApproval(log)) {
|
|
498
582
|
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>\"".');
|
|
499
583
|
} else add(G, "PASS", "The approval stop is logged with the user's reply");
|
|
500
584
|
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.`);
|
|
@@ -503,7 +587,7 @@ const usesKind = (kind) => [...usedBlocks].some((b) => b.startsWith(`${kind}/`))
|
|
|
503
587
|
}
|
|
504
588
|
|
|
505
589
|
/* ------------------------------------------------------------------ REPORT */
|
|
506
|
-
const order = ["IMAGES", "TELLS", "MOTION", "HERO", "TYPE", "PLAN", "LOG"];
|
|
590
|
+
const order = ["IMAGES", "TELLS", "MOTION", "HERO", "FILM", "TYPE", "PLAN", "LOG"];
|
|
507
591
|
console.log(`Struq check: ${root}\n`);
|
|
508
592
|
for (const g of order) {
|
|
509
593
|
const rows = results.filter((r) => r.group === g);
|