@struq/skill 0.4.0 → 0.4.2
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/package.json +4 -3
- package/skills/struq/blocks/CATALOG.md +1 -1
- package/skills/struq/blocks/catalog.json +2 -2
- package/skills/struq/blocks/kit/HeroCanvas.tsx +2 -2
- package/skills/struq/canon/canon.json +2 -2
- package/skills/struq/references/tells.md +3 -0
- package/skills/struq/scripts/check.mjs +21 -0
- package/skills/struq/scripts/images.mjs +33 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@struq/skill",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Install the free Struq skill into Claude Code, Cursor, Codex and other AI coding tools: websites at the level of a design studio, built by your own AI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Struq (https://struq.nl)",
|
|
@@ -35,8 +35,9 @@
|
|
|
35
35
|
"node": ">=18"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
|
-
"test": "node --test test/install.test.mjs test/check.test.mjs test/packs.test.mjs test/tokens.test.mjs test/blocks.test.mjs",
|
|
39
|
-
"release:blocks": "node scripts/release-blocks.mjs"
|
|
38
|
+
"test": "node --test test/install.test.mjs test/check.test.mjs test/packs.test.mjs test/tokens.test.mjs test/blocks.test.mjs test/plugin.test.mjs",
|
|
39
|
+
"release:blocks": "node scripts/release-blocks.mjs",
|
|
40
|
+
"release:plugin": "node scripts/build-plugin.mjs"
|
|
40
41
|
},
|
|
41
42
|
"publishConfig": {
|
|
42
43
|
"access": "public"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Block catalogue
|
|
2
2
|
|
|
3
|
-
Generated by scripts/release-blocks.mjs from Struq
|
|
3
|
+
Generated by scripts/release-blocks.mjs from Struq origin/main at a2b7d56. Do not edit: run the release script again.
|
|
4
4
|
44 admitted blocks. Install one with `node <skill-dir>/scripts/blocks.mjs add <slug>`; read its file for the props.
|
|
5
5
|
Every block works light and dark; a variant is a different composition of the same idea, passed as `variant`.
|
|
6
6
|
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
// <div className="relative z-10"><h1 data-quiet>...</h1><p data-quiet>...</p></div>
|
|
24
24
|
// </section>
|
|
25
25
|
//
|
|
26
|
-
// A scene
|
|
27
|
-
// components/struq/scenes/<name>.ts exporting createScene (see scenes/types.ts) and add it to SCENES.
|
|
26
|
+
// A scene of your own: write it as
|
|
27
|
+
// components/struq/kit/scenes/<name>.ts exporting createScene (see scenes/types.ts) and add it to SCENES.
|
|
28
28
|
import Image from "next/image";
|
|
29
29
|
import { useEffect, useRef } from "react";
|
|
30
30
|
import { gsap } from "./gsap";
|
|
@@ -45,6 +45,9 @@ connected); the rest are judged by eye. The fix is beside each.
|
|
|
45
45
|
- Emoji and the cliche icons (sparkles, rocket, lightning, bulb, wand). Fix: Struq Icons, functional only. (check)
|
|
46
46
|
- Invented facts: testimonials, client logos, stats, awards, prices, addresses, hours, bios, quotes the brief did not
|
|
47
47
|
give. Fix: leave them out and list them under Assumptions.
|
|
48
|
+
- **Notes to the client on the page:** "het e-mailadres volgt zodra de studio het aanlevert", "coming soon", "[telefoon]",
|
|
49
|
+
"to be confirmed". Honest, but a visitor reads it as an unfinished site. Fix: leave the missing fact out of the page
|
|
50
|
+
(drop the line, the column or the link) and list it under Assumptions in STRUQ.md. (check)
|
|
48
51
|
|
|
49
52
|
## Judged by eye
|
|
50
53
|
|
|
@@ -154,6 +154,22 @@ const plan = read("STRUQ.md");
|
|
|
154
154
|
}
|
|
155
155
|
if (brokenPhotos.length) add(G, "FAIL", "These image files are not real images (an error page saved as .webp?). Delete them and run images.mjs again.", brokenPhotos.map(rel));
|
|
156
156
|
|
|
157
|
+
// A file whose size differs from the one images.mjs recorded is not the photo its alt and caption
|
|
158
|
+
// describe: a run of another pack, or a hand copy, left it under the same name.
|
|
159
|
+
const manifestFile = join(pub, "images", "struq-pack.json");
|
|
160
|
+
if (existsSync(manifestFile)) {
|
|
161
|
+
let recorded = [];
|
|
162
|
+
try {
|
|
163
|
+
recorded = JSON.parse(readFileSync(manifestFile, "utf8")).images ?? [];
|
|
164
|
+
} catch {
|
|
165
|
+
add(G, "WARN", "public/images/struq-pack.json is not readable JSON. Run images.mjs again.");
|
|
166
|
+
}
|
|
167
|
+
const mismatched = recorded
|
|
168
|
+
.filter((m) => typeof m.bytes === "number" && existsSync(join(root, m.file)) && statSync(join(root, m.file)).size !== m.bytes)
|
|
169
|
+
.map((m) => m.file);
|
|
170
|
+
if (mismatched.length) add(G, "FAIL", "These photos are not the ones their alt and caption describe (left by an earlier pack or copied by hand). Run images.mjs again with the pack you chose.", mismatched);
|
|
171
|
+
}
|
|
172
|
+
|
|
157
173
|
// Each pack reference (images.card[2], images.card.slice(0, 5)) or own image tag counts once.
|
|
158
174
|
const used = own.reduce((n, f) => n + (f.text.match(/images\.(hero|band|card|column|detail|backdrop)\b|<img\b|src=["'{`]\/images\//g) ?? []).length, 0);
|
|
159
175
|
const total = Object.values(slotCount).reduce((a, b) => a + b, 0);
|
|
@@ -217,6 +233,11 @@ const plan = read("STRUQ.md");
|
|
|
217
233
|
};
|
|
218
234
|
const ownOnly = { scope: own };
|
|
219
235
|
|
|
236
|
+
// Copy
|
|
237
|
+
tell("Notes to the client on the page",
|
|
238
|
+
/volg(?:t|en) zodra|zodra (?:de studio|de klant|je|jullie|we)\b[^"'<>]{0,40}(?:aanlever|doorgeef|doorgeven|binnen)|nog aan te leveren|wordt nog aangevuld|lorem ipsum|\bcoming soon\b|to be confirmed|\bTBC\b|\[(?:naam|adres|telefoon|e-?mail|name|phone|address)\]/i,
|
|
239
|
+
"a line written to the client or the developer is not copy. Leave a missing fact out of the page and list it under Assumptions: in STRUQ.md.");
|
|
240
|
+
|
|
220
241
|
// Type and labels
|
|
221
242
|
tell("Eyebrows and uppercase labels", /\buppercase\b|text-transform:\s*uppercase|\b(eyebrow|kicker|overline)\b/i,
|
|
222
243
|
"small uppercase labels above headings, uppercase marquees and tracked caps are AI tells. Delete the label; the heading, a hairline rule or a jump in scale does the wayfinding. A deliberate all-caps wordmark passes when its element carries data-wordmark.",
|
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
// 4. If struq.nl cannot be reached, or a file fails its check, it writes a graded fallback plate
|
|
19
19
|
// in the site palette for that slot instead, so no frame is ever empty, and says so.
|
|
20
20
|
//
|
|
21
|
-
// Node 18 or later, no dependencies. Safe to run again:
|
|
22
|
-
|
|
21
|
+
// Node 18 or later, no dependencies. Safe to run again: a file is kept only when the last run
|
|
22
|
+
// downloaded it from the same source (struq-pack.json records each one); a file from another pack
|
|
23
|
+
// is replaced, and a slot file the new pack no longer fills is removed.
|
|
24
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
23
25
|
import { join, resolve } from "node:path";
|
|
24
26
|
import { appendLog } from "./log.mjs";
|
|
25
27
|
import { fitImages, packForBrief, packReason } from "./packs.mjs";
|
|
@@ -201,6 +203,28 @@ ${body}
|
|
|
201
203
|
return file;
|
|
202
204
|
}
|
|
203
205
|
|
|
206
|
+
/** file -> source from the last run's struq-pack.json; empty when there was no run or it is unreadable. */
|
|
207
|
+
function readManifest(outDir) {
|
|
208
|
+
try {
|
|
209
|
+
const data = JSON.parse(readFileSync(join(outDir, "struq-pack.json"), "utf8"));
|
|
210
|
+
return new Map((data.images ?? []).map((i) => [i.file, i.source]));
|
|
211
|
+
} catch {
|
|
212
|
+
return new Map();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Removes the slot files (hero-1.webp, card-3.svg, ...) this run did not write, so a photo or plate
|
|
218
|
+
* from an earlier pack can never sit under this pack's caption. Other files in public/images stay.
|
|
219
|
+
*/
|
|
220
|
+
function removeStale(outDir, manifest) {
|
|
221
|
+
const written = new Set(manifest.map((m) => m.file.split("/").pop()));
|
|
222
|
+
const slotFile = new RegExp(`^(${SLOTS.join("|")})-\\d+\\.(webp|svg)$`);
|
|
223
|
+
for (const name of readdirSync(outDir)) {
|
|
224
|
+
if (slotFile.test(name) && !written.has(name)) unlinkSync(join(outDir, name));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
204
228
|
async function list() {
|
|
205
229
|
const index = await getJson(API);
|
|
206
230
|
console.log(`${index.packCount} packs, ${index.totalImages} images, ${index.license}\n`);
|
|
@@ -220,6 +244,7 @@ async function main() {
|
|
|
220
244
|
const outDir = join(root, "public", "images");
|
|
221
245
|
mkdirSync(outDir, { recursive: true });
|
|
222
246
|
const palette = readPalette();
|
|
247
|
+
const previous = readManifest(outDir);
|
|
223
248
|
|
|
224
249
|
let pack = null;
|
|
225
250
|
let droppedImages = [];
|
|
@@ -268,7 +293,9 @@ async function main() {
|
|
|
268
293
|
while (cursor < jobs.length) {
|
|
269
294
|
const job = jobs[cursor++];
|
|
270
295
|
const target = join(outDir, `${job.name}.webp`);
|
|
271
|
-
const
|
|
296
|
+
const source = imageUrl(job.img);
|
|
297
|
+
const kept = previous.get(`public/images/${job.name}.webp`) === source && fileIsImage(target);
|
|
298
|
+
const ok = kept || (await download(source, target));
|
|
272
299
|
if (ok) {
|
|
273
300
|
real++;
|
|
274
301
|
entries.push({
|
|
@@ -282,7 +309,8 @@ async function main() {
|
|
|
282
309
|
description: job.img.description ?? "",
|
|
283
310
|
negativeSpace: job.img.negativeSpace ?? "",
|
|
284
311
|
});
|
|
285
|
-
|
|
312
|
+
// bytes lets check.mjs tell this file from one an earlier pack left under the same name.
|
|
313
|
+
manifest.push({ file: `public/images/${job.name}.webp`, id: job.img.id, slot: job.img.slot, source, bytes: statSync(target).size });
|
|
286
314
|
} else {
|
|
287
315
|
plates++;
|
|
288
316
|
const size = [job.img.width ?? FALLBACK_SIZE[job.img.slot][0], job.img.height ?? FALLBACK_SIZE[job.img.slot][1]];
|
|
@@ -326,6 +354,7 @@ async function main() {
|
|
|
326
354
|
}
|
|
327
355
|
|
|
328
356
|
entries.sort((a, b) => SLOTS.indexOf(a.slot) - SLOTS.indexOf(b.slot) || a.order - b.order);
|
|
357
|
+
removeStale(outDir, manifest);
|
|
329
358
|
writeFileSync(join(outDir, "struq-pack.json"), JSON.stringify({ pack: slug, images: manifest }, null, 2) + "\n");
|
|
330
359
|
const module = writeModule(entries, slug, !pack, imageFit);
|
|
331
360
|
|