@struq/skill 0.3.0 → 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.
- package/README.md +22 -36
- package/package.json +3 -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 +2 -2
- 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
|
@@ -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
|
+
}
|
|
@@ -1,309 +1,134 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Struq canon: Struq's
|
|
3
|
-
//
|
|
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.
|
|
4
5
|
//
|
|
5
|
-
// node <skill-dir>/scripts/canon.mjs list
|
|
6
|
-
// node <skill-dir>/scripts/canon.mjs palette <
|
|
7
|
-
// node <skill-dir>/scripts/canon.mjs type <
|
|
8
|
-
// node <skill-dir>/scripts/canon.mjs
|
|
9
|
-
// node <skill-dir>/scripts/canon.mjs system <slug> prints the section order and its refs
|
|
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
10
|
// add --dir <project> to run from elsewhere
|
|
11
11
|
//
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
import {
|
|
16
|
-
import { join, resolve } from "node:path";
|
|
17
|
-
import { googleName } from "./font-names.mjs";
|
|
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";
|
|
18
16
|
import { appendLog } from "./log.mjs";
|
|
17
|
+
import { fontHrefs, headFamily, lightness, paletteTokens } from "./tokens.mjs";
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const API = `${(process.env.STRUQ_API_BASE || "https://struq.nl").replace(/\/+$/, "")}/api`;
|
|
19
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const CANON = JSON.parse(readFileSync(join(here, "..", "canon", "canon.json"), "utf8"));
|
|
23
21
|
const args = process.argv.slice(2);
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
};
|
|
28
|
-
const root = resolve(opt("--dir") ?? process.cwd());
|
|
29
|
-
const [cmd, slugArg] = args.filter((a, i) => !a.startsWith("--") && !["--dir", "--display", "--body", "--accent"].includes(args[i - 1]));
|
|
30
|
-
|
|
31
|
-
// Struq's own rules win over a canon entry: these faces never ship (SKILL.md, the tell list).
|
|
32
|
-
const BANNED = new Set(["Inter", "Roboto", "Arial", "Helvetica", "Open Sans", "Poppins", "Montserrat", "Lato", "Geist", "Space Grotesk", "Geist Mono", "JetBrains Mono", "Space Mono", "IBM Plex Mono", "Roboto Mono"]);
|
|
33
|
-
// The text face that replaces a banned one, by the display face it sits under.
|
|
34
|
-
const BODY_FOR = { Fraunces: "Urbanist", Newsreader: "Manrope", Sora: "Instrument Sans", Syne: "Hanken Grotesk", Manrope: "Figtree" };
|
|
35
|
-
// Commercial faces in the canon, and their free stand-ins.
|
|
36
|
-
const STAND_IN = { "Neue Haas Grotesk Text": "General Sans", "Neue Haas Grotesk": "General Sans", "PP Neue Montreal": "General Sans", "Söhne": "General Sans", Canela: "Newsreader", "PP Editorial New": "Instrument Serif", "GT Sectra": "Fraunces" };
|
|
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));
|
|
37
25
|
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
if (!
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
async function canon(filter) {
|
|
44
|
-
const all = [];
|
|
45
|
-
for (let page = 1; page <= 10; page++) {
|
|
46
|
-
const d = await getJson(`${API}/assets?filter=${filter}&page=${page}`);
|
|
47
|
-
all.push(...(d.items ?? []));
|
|
48
|
-
if (!d.hasMore) break;
|
|
49
|
-
}
|
|
50
|
-
return all;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function parseOklch(v) {
|
|
54
|
-
const m = String(v).match(/oklch\(\s*([\d.]+%?)\s+([\d.]+)\s+([\d.]+)/i);
|
|
55
|
-
if (!m) return null;
|
|
56
|
-
const L = m[1].endsWith("%") ? parseFloat(m[1]) / 100 : parseFloat(m[1]);
|
|
57
|
-
return { L, C: parseFloat(m[2]), H: parseFloat(m[3]) };
|
|
58
|
-
}
|
|
59
|
-
function hexLightness(v) {
|
|
60
|
-
const m = String(v).match(/^#([0-9a-f]{6})$/i);
|
|
61
|
-
if (!m) return null;
|
|
62
|
-
const [r, g, b] = [0, 2, 4].map((i) => parseInt(m[1].slice(i, i + 2), 16) / 255);
|
|
63
|
-
return (Math.max(r, g, b) + Math.min(r, g, b)) / 2;
|
|
64
|
-
}
|
|
65
|
-
const lightness = (v) => parseOklch(v)?.L ?? hexLightness(v) ?? 0.5;
|
|
66
|
-
const isCool = (v) => {
|
|
67
|
-
const o = parseOklch(v);
|
|
68
|
-
return o ? o.C > 0.06 && o.H >= 220 && o.H <= 320 : false;
|
|
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;
|
|
69
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);
|
|
70
37
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
console.log(`PALETTES (${palettes.length}): slug, mood, ground, accent`);
|
|
75
|
-
for (const p of palettes) {
|
|
76
|
-
const c = p.data?.colors ?? {};
|
|
77
|
-
const ground = lightness(c.background) > 0.5 ? "light" : "dark";
|
|
78
|
-
const note = isCool(c.accent) ? " (blue or purple accent: only for a brand that is that colour)" : "";
|
|
79
|
-
console.log(` ${p.slug.padEnd(38)} ${(p.data?.mood ?? "").padEnd(36)} ${ground.padEnd(6)}${note}`);
|
|
80
|
-
}
|
|
81
|
-
console.log(`\nPAIRINGS (${types.length}): slug, display + body as Struq builds them`);
|
|
82
|
-
for (const t of types) {
|
|
83
|
-
const plan = planType(t.data.display.name, t.data.body.name);
|
|
84
|
-
console.log(` ${t.slug.padEnd(28)} ${plan.display} + ${plan.body}${plan.notes.length ? ` (${plan.notes.join("; ")})` : ""}`);
|
|
85
|
-
}
|
|
86
|
-
console.log(`\nDESIGN SYSTEMS (${systems.length}): slug, section order`);
|
|
87
|
-
for (const s of systems) console.log(` ${s.slug.padEnd(22)} ${(s.data?.silhouette?.sectionOrder ?? []).join(" > ")}`);
|
|
88
|
-
try {
|
|
89
|
-
const packs = await getJson(`${API}/media/packs`);
|
|
90
|
-
console.log(`\nBEELDBANK PACKS (${packs.packCount}): use with images.mjs`);
|
|
91
|
-
for (const p of packs.packs) console.log(` ${p.slug.padEnd(10)} ${p.description.slice(0, 100)}`);
|
|
92
|
-
} catch (e) {
|
|
93
|
-
console.log(`\nBEELDBANK: ${e.message}`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/* --------------------------------------------------------------- palette */
|
|
98
|
-
async function palette(slug) {
|
|
99
|
-
const items = await canon("palette");
|
|
100
|
-
const p = items.find((i) => i.slug === slug);
|
|
101
|
-
if (!p) throw new Error(`Unknown palette "${slug}". Run: canon.mjs list`);
|
|
102
|
-
const c = p.data.colors;
|
|
103
|
-
const dark = lightness(c.background) < 0.5;
|
|
104
|
-
const accentInk = lightness(c.accent) > 0.62 ? (dark ? c.background : c.text) : dark ? c.text : c.background;
|
|
105
|
-
const cssPath = ["app/globals.css", "src/app/globals.css"].map((f) => join(root, f)).find(existsSync);
|
|
106
|
-
if (!cssPath) throw new Error("No app/globals.css. Run recipes.mjs first.");
|
|
107
|
-
let css = readFileSync(cssPath, "utf8");
|
|
108
|
-
const weight = css.match(/--display-weight:\s*(\d{3})/)?.[1] ?? "300";
|
|
109
|
-
const block = `:root {
|
|
110
|
-
/* canon palette: ${p.slug} (${p.data.mood}) */
|
|
111
|
-
--surface-0: ${c.background}; /* the page */
|
|
112
|
-
--surface-1: ${c.surface}; /* one step: panels, the showpiece ground */
|
|
113
|
-
--surface-2: color-mix(in oklch, var(--surface-1), var(--ink) ${dark ? 9 : 7}%); /* image wells, hover grounds */
|
|
114
|
-
--surface-3: color-mix(in oklch, var(--surface-1), var(--ink) ${dark ? 18 : 16}%); /* hairline accents, never text */
|
|
115
|
-
--ink: ${c.text};
|
|
116
|
-
--ink-muted: color-mix(in oklch, var(--ink) ${dark ? 70 : 66}%, var(--surface-0)); /* secondary text */
|
|
117
|
-
--accent: ${c.accent}; /* the one accent: the key CTA, one highlight per screen */
|
|
118
|
-
--accent-ink: ${accentInk}; /* text on the accent */
|
|
119
|
-
--accent-2: ${c.accentAlt ?? c.surface}; /* a second ground, never a second highlight colour */
|
|
120
|
-
--line: color-mix(in oklch, var(--ink) 13%, transparent); /* hairlines */
|
|
121
|
-
--on-photo: ${dark ? c.text : c.background}; /* type over a photo, with a scrim */
|
|
122
|
-
/* the contrast scene: the palette's inverse and field roles when it has them, else text and ground swapped */
|
|
123
|
-
--inverse: ${c.inverse ?? (dark ? c.surface : c.text)};
|
|
124
|
-
--on-inverse: ${c.onInverse ?? c.inverseText ?? (dark ? c.text : c.background)};
|
|
125
|
-
--field: ${c.field ?? c.accent};
|
|
126
|
-
--on-field: ${c.onField ?? c.fieldText ?? accentInk};
|
|
127
|
-
--display-weight: ${weight};
|
|
128
|
-
}`;
|
|
129
|
-
const re = /(STRUQ TOKENS[\s\S]*?\*\/\s*):root\s*\{[\s\S]*?\n\}/;
|
|
130
|
-
if (!re.test(css)) throw new Error(`No STRUQ TOKENS block in ${cssPath}. Run recipes.mjs, or merge the block from recipes/app/globals.css.`);
|
|
131
|
-
css = css.replace(re, `$1${block}`);
|
|
132
|
-
if (!css.includes("--color-accent-2")) css = css.replace(" --color-line: var(--line);", " --color-line: var(--line);\n --color-accent-2: var(--accent-2);");
|
|
133
|
-
// Projects started before the contrast scene existed get its colour tokens too.
|
|
134
|
-
if (!css.includes("--color-inverse")) {
|
|
135
|
-
css = css.replace(" --color-line: var(--line);", " --color-line: var(--line);\n --color-inverse: var(--inverse);\n --color-on-inverse: var(--on-inverse);\n --color-field: var(--field);\n --color-on-field: var(--on-field);");
|
|
136
|
-
}
|
|
137
|
-
writeFileSync(cssPath, css);
|
|
138
|
-
console.log(`Wrote palette ${p.slug} to ${cssPath.replace(root, ".")}`);
|
|
139
|
-
if (isCool(c.accent)) console.log('NOTE blue or purple accent: write "Cool hue: declared because <the brand colour>" in STRUQ.md, or pick a warmer palette.');
|
|
140
|
-
console.log(`STRUQ.md line: Palette: ${p.slug} (${p.data.mood})`);
|
|
141
|
-
appendLog(root, "Step 4 Canon palette", [
|
|
142
|
-
`canon.mjs wrote palette ${p.slug} (${p.data.mood}) into the STRUQ TOKENS block.`,
|
|
143
|
-
`Ground ${dark ? "dark" : "light"}, accent ${c.accent}.${isCool(c.accent) ? " Blue or purple accent: needs a Cool hue declaration." : ""}`,
|
|
144
|
-
]);
|
|
145
|
-
console.log("OK palette written");
|
|
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}`;
|
|
146
41
|
}
|
|
147
42
|
|
|
148
|
-
|
|
149
|
-
function planType(display, body) {
|
|
150
|
-
const notes = [];
|
|
151
|
-
let d = display;
|
|
152
|
-
let b = body;
|
|
153
|
-
if (STAND_IN[d]) notes.push(`${d} is commercial, ${STAND_IN[d]} stands in`), (d = STAND_IN[d]);
|
|
154
|
-
if (STAND_IN[b]) notes.push(`${b} is commercial, ${STAND_IN[b]} stands in`), (b = STAND_IN[b]);
|
|
155
|
-
if (BANNED.has(d)) notes.push(`${d} is off the Struq list: pick another pairing`), (d = null);
|
|
156
|
-
if (BANNED.has(b)) {
|
|
157
|
-
const swap = BODY_FOR[d] ?? "Urbanist";
|
|
158
|
-
notes.push(`${b} is off the Struq list, ${swap} replaces it`);
|
|
159
|
-
b = swap;
|
|
160
|
-
}
|
|
161
|
-
return { display: d ?? "(none)", body: b, notes, ok: Boolean(d) };
|
|
162
|
-
}
|
|
43
|
+
const BLOCK = /\/\* STRUQ TOKENS[^*]*\*\/\s*\[data-blocks\]\s*\{[\s\S]*?\n\}/;
|
|
163
44
|
|
|
164
|
-
function
|
|
165
|
-
const
|
|
166
|
-
|
|
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;
|
|
167
49
|
}
|
|
168
50
|
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
const css =
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
style: m[1].match(/font-style:\s*(\w+)/)?.[1] ?? "normal",
|
|
176
|
-
})).filter((f) => f.url && f.weight);
|
|
177
|
-
if (!faces.length) return null;
|
|
178
|
-
const dir = join(root, existsSync(join(root, "src", "app")) ? "src/app" : "app", "fonts");
|
|
179
|
-
mkdirSync(dir, { recursive: true });
|
|
180
|
-
const src = [];
|
|
181
|
-
for (const f of faces) {
|
|
182
|
-
const file = `${slug}-${f.weight}${f.style === "italic" ? "-italic" : ""}.woff2`;
|
|
183
|
-
const res = await fetch(f.url.startsWith("//") ? `https:${f.url}` : f.url, { signal: AbortSignal.timeout(30000) });
|
|
184
|
-
if (!res.ok) continue;
|
|
185
|
-
writeFileSync(join(dir, file), Buffer.from(await res.arrayBuffer()));
|
|
186
|
-
src.push({ path: `./fonts/${file}`, weight: f.weight, style: f.style });
|
|
187
|
-
}
|
|
188
|
-
return src.length ? src : null;
|
|
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;
|
|
189
57
|
}
|
|
190
58
|
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
const name = googleName(canonName, google) ?? canonName;
|
|
196
|
-
const g = google?.[name];
|
|
197
|
-
if (g) {
|
|
198
|
-
const ident = name.replace(/\s+/g, "_");
|
|
199
|
-
const parts = [`subsets: ["latin"]`];
|
|
200
|
-
const italicOnly = role === "accent";
|
|
201
|
-
if (!g.weights.includes("variable")) {
|
|
202
|
-
const wanted = g.weights.filter((w) => ["300", "400", "500"].includes(w));
|
|
203
|
-
parts.push(`weight: ${JSON.stringify(wanted.length ? wanted : [g.weights[0]])}`);
|
|
204
|
-
}
|
|
205
|
-
const axes = (g.axes ?? []).map((a) => a.tag).filter((t) => ["opsz", "SOFT"].includes(t));
|
|
206
|
-
if (axes.length && g.weights.includes("variable")) parts.push(`axes: ${JSON.stringify(axes)}`);
|
|
207
|
-
if (italicOnly && g.styles.includes("italic")) parts.push(`style: ["italic"]`);
|
|
208
|
-
else if (role === "display" && g.styles.includes("italic")) parts.push(`style: ["normal", "italic"]`);
|
|
209
|
-
parts.push(`variable: "${variable}"`, `display: "swap"`);
|
|
210
|
-
const light = g.weights.includes("300") ? 300 : g.axes?.find((a) => a.tag === "wght")?.min <= 300 ? 300 : 400;
|
|
211
|
-
return { kind: "google", id, ident, family: name, code: `export const ${id} = ${ident}({ ${parts.join(", ")} });`, light };
|
|
212
|
-
}
|
|
213
|
-
const local = await fontshare(canonName, role === "accent" ? ["400"] : ["300", "400", "500"]);
|
|
214
|
-
if (local) {
|
|
215
|
-
const light = local.some((s) => s.weight === "300") ? 300 : 400;
|
|
216
|
-
const src = local.map((s) => `{ path: "${s.path}", weight: "${s.weight}", style: "${s.style}" }`).join(", ");
|
|
217
|
-
return { kind: "local", id, family: canonName, code: `export const ${id} = localFont({ src: [${src}], variable: "${variable}", display: "swap" });`, light };
|
|
218
|
-
}
|
|
219
|
-
return null;
|
|
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 };
|
|
220
63
|
}
|
|
221
64
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
let body = opt("--body");
|
|
225
|
-
let accent = opt("--accent");
|
|
226
|
-
let label = "custom";
|
|
227
|
-
if (slug) {
|
|
228
|
-
const items = await canon("typography");
|
|
229
|
-
const t = items.find((i) => i.slug === slug);
|
|
230
|
-
if (!t) throw new Error(`Unknown pairing "${slug}". Run: canon.mjs list`);
|
|
231
|
-
display = t.data.display.name;
|
|
232
|
-
body = t.data.body.name;
|
|
233
|
-
accent = t.data.accent?.name;
|
|
234
|
-
label = t.slug;
|
|
235
|
-
}
|
|
236
|
-
if (!display || !body) throw new Error('Usage: canon.mjs type <slug>, or type --display "Name" --body "Name" [--accent "Name"]');
|
|
237
|
-
const plan = planType(display, body);
|
|
238
|
-
for (const n of plan.notes) console.log(`NOTE ${n}`);
|
|
239
|
-
if (!plan.ok) throw new Error("The display face is off the Struq list. Pick another pairing (canon.mjs list).");
|
|
240
|
-
const google = googleData();
|
|
241
|
-
if (!google) console.log("NOTE next is not installed here, so Google faces cannot be checked; install first for a sure result.");
|
|
242
|
-
const d = await faceCode(plan.display, "display", google);
|
|
243
|
-
const b = await faceCode(plan.body, "body", google);
|
|
244
|
-
if (!d || !b) throw new Error(`Could not load ${!d ? plan.display : plan.body} from Google Fonts or Fontshare. Pick another pairing.`);
|
|
245
|
-
// The italic accent: its own face when the canon names one apart from display and body; the
|
|
246
|
-
// display face's italic otherwise (font-accent falls back to it in globals.css).
|
|
247
|
-
const accentName = accent && !BANNED.has(accent) && accent !== plan.display && accent !== plan.body ? accent : null;
|
|
248
|
-
const a = accentName ? await faceCode(accentName, "accent", google) : null;
|
|
249
|
-
if (accentName && !a) console.log(`NOTE the accent face ${accentName} could not be loaded; the display italic carries the accent word.`);
|
|
250
|
-
const faces = [d, b, a].filter(Boolean);
|
|
251
|
-
const imports = [];
|
|
252
|
-
const gIdents = faces.filter((f) => f.kind === "google").map((f) => f.ident);
|
|
253
|
-
if (gIdents.length) imports.push(`import { ${[...new Set(gIdents)].join(", ")} } from "next/font/google";`);
|
|
254
|
-
if (faces.some((f) => f.kind === "local")) imports.push(`import localFont from "next/font/local";`);
|
|
65
|
+
function writeFonts(t) {
|
|
66
|
+
const hrefs = fontHrefs(t, CANON);
|
|
255
67
|
const file = join(root, existsSync(join(root, "src", "app")) ? "src/app" : "app", "fonts.ts");
|
|
256
68
|
writeFileSync(
|
|
257
69
|
file,
|
|
258
|
-
`// app/fonts.ts: written by the Struq skill (
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
${faces.map((f) => f.code).join("\n\n")}
|
|
264
|
-
|
|
265
|
-
/** Every face's CSS variable, for the <html> className in app/layout.tsx. */
|
|
266
|
-
export const fontVariables = [${faces.map((f) => f.id).join(", ")}].map((f) => f.variable).join(" ");
|
|
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)};
|
|
267
75
|
`,
|
|
268
76
|
);
|
|
269
|
-
|
|
270
|
-
if (cssPath) {
|
|
271
|
-
let css = readFileSync(cssPath, "utf8").replace(/--display-weight:\s*\d{3}/, `--display-weight: ${d.light}`);
|
|
272
|
-
if (!css.includes("--font-accent")) {
|
|
273
|
-
css = css.replace(/(\s*--font-body:[^\n]*\n)/, `$1 --font-accent: var(--face-accent, var(--face-display)), ui-serif, Georgia, serif;\n`);
|
|
274
|
-
}
|
|
275
|
-
writeFileSync(cssPath, css);
|
|
276
|
-
}
|
|
277
|
-
console.log(`Wrote ${file.replace(root, ".")}: ${d.family} (${d.kind}) + ${b.family} (${b.kind})${a ? ` + accent ${a.family} italic (${a.kind})` : ""}, display weight ${d.light}`);
|
|
278
|
-
console.log(`STRUQ.md line: Type: ${label}: ${d.family} + ${b.family}${a ? `, accent ${a.family}` : ""}`);
|
|
279
|
-
appendLog(root, "Step 4 Canon type", [
|
|
280
|
-
`canon.mjs wrote app/fonts.ts: ${d.family} (${d.kind}) over ${b.family} (${b.kind})${a ? `, accent ${a.family} italic` : ""}, pairing ${label}.`,
|
|
281
|
-
`Display weight ${d.light}.${plan.notes.length ? ` Overrides: ${plan.notes.join("; ")}.` : ""}`,
|
|
282
|
-
]);
|
|
283
|
-
console.log("OK type written");
|
|
77
|
+
return { file, hrefs };
|
|
284
78
|
}
|
|
285
79
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
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
|
+
};
|
|
300
121
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
+
}
|
|
305
134
|
}
|
|
306
|
-
run().catch((e) => {
|
|
307
|
-
console.error(`FAIL ${e.message}`);
|
|
308
|
-
process.exitCode = 1;
|
|
309
|
-
});
|