@struq/skill 0.4.2 → 0.5.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 +167 -59
- package/package.json +2 -2
- 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/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 +94 -11
- package/skills/struq/scripts/film-check.mjs +997 -0
- package/skills/struq/scripts/gate.mjs +146 -0
- package/skills/struq/scripts/packs.mjs +73 -12
|
@@ -9,22 +9,35 @@
|
|
|
9
9
|
// <HeroOpener image={images.hero[0]} title="Koken op" accent="vuur." lead="..." cta={{ href: "#reserveren", label: "Reserveer een tafel" }} header={<SiteHeader />} />
|
|
10
10
|
// <HeroOpener image={images.hero[0]} split={["Vuur", "Noord"]} lead="..." cta={...} />
|
|
11
11
|
//
|
|
12
|
-
//
|
|
12
|
+
// <HeroOpener image={images.hero[0]} title="Koken op vuur." accent="vuur" lead="..." cta={...} />
|
|
13
|
+
//
|
|
14
|
+
// The line is the display face at poster size, with at most one italic accent word: the site's one accent word, marked
|
|
15
|
+
// in place when the title holds it (the same mark as the Statement and the Close, kept whole by SplitText's line cut),
|
|
16
|
+
// or set after the title when it does not. The flat scrim
|
|
13
17
|
// keeps the copy at 4.5:1; never a gradient. Reduced motion and phones: the composed still, no pin.
|
|
14
18
|
// The section after the opener slides over it: globals.css gives it a ground and a higher layer.
|
|
19
|
+
//
|
|
20
|
+
// The entrance (the line rising through its masks, the split words, the lead and the CTA) waits for
|
|
21
|
+
// any preloader or page transition to start leaving (whenUncovered, from the helper the frames share),
|
|
22
|
+
// and plays at once when nothing covers the page.
|
|
15
23
|
import { useRef } from "react";
|
|
24
|
+
import { findAccent, withAccent } from "../kit/accent";
|
|
16
25
|
import { HeroCanvas } from "../kit/HeroCanvas";
|
|
17
26
|
import { MagneticButton } from "../kit/MagneticButton";
|
|
18
|
-
import {
|
|
19
|
-
import { gsap, useGSAP } from "../kit/gsap";
|
|
27
|
+
import { SplitText, gsap, useGSAP } from "../kit/gsap";
|
|
20
28
|
import type { StruqImage } from "../kit/images";
|
|
29
|
+
import { whenUncovered } from "../preloaders/preloader-slices-uncovered";
|
|
21
30
|
|
|
22
31
|
type Props = {
|
|
23
32
|
/** The beeldbank or client photo. Without one (imageFit "none"), the opener is typographic on the inverse ground over the dots scene. */
|
|
24
33
|
image?: StruqImage;
|
|
25
34
|
/** The poster line ("across"). */
|
|
26
35
|
title?: string;
|
|
27
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* The site's one accent word, in the pairing's italic accent face. A word the title holds is marked where it first
|
|
38
|
+
* stands (whole word, any case, punctuation outside), as the Statement and the Close mark it; a word the title does
|
|
39
|
+
* not hold is set after the title, as before.
|
|
40
|
+
*/
|
|
28
41
|
accent?: string;
|
|
29
42
|
/** Two words for the split wordmark; replaces title. */
|
|
30
43
|
split?: [string, string];
|
|
@@ -46,6 +59,7 @@ export function HeroOpener({ image, title, accent, split, lead, cta, header, scr
|
|
|
46
59
|
// The lead goes on the photo's calm side; the CTA takes the other. Over a busy frame (no calm
|
|
47
60
|
// side known) the lead gets a flat backing at a fixed opacity and is set a step larger.
|
|
48
61
|
const side = calm ?? (image?.negativeSpace === "left" || image?.negativeSpace === "right" ? image.negativeSpace : image ? "none" : "left");
|
|
62
|
+
const inTitle = Boolean(title && findAccent(title, accent));
|
|
49
63
|
const stage = useRef<HTMLElement>(null);
|
|
50
64
|
|
|
51
65
|
useGSAP(
|
|
@@ -53,12 +67,48 @@ export function HeroOpener({ image, title, accent, split, lead, cta, header, scr
|
|
|
53
67
|
const el = stage.current;
|
|
54
68
|
if (!el) return;
|
|
55
69
|
const q = gsap.utils.selector(el);
|
|
70
|
+
const title = q("[data-opener-title]")[0] as HTMLElement | undefined;
|
|
56
71
|
const mm = gsap.matchMedia();
|
|
72
|
+
mm.add("(prefers-reduced-motion: reduce)", () => {
|
|
73
|
+
if (!title) return;
|
|
74
|
+
title.dataset.ready = "1";
|
|
75
|
+
gsap.from(title, { autoAlpha: 0, duration: 0.6 });
|
|
76
|
+
});
|
|
57
77
|
mm.add("(prefers-reduced-motion: no-preference)", () => {
|
|
78
|
+
// Every part is set in its start state now and the whole entrance plays once nothing covers the page.
|
|
79
|
+
let uncovered = false;
|
|
80
|
+
const intro = gsap.timeline({ paused: true });
|
|
58
81
|
if (split) {
|
|
59
|
-
|
|
82
|
+
intro.from(q("[data-split-word]"), { yPercent: 110, duration: 1.4, ease: "expo.out", stagger: 0.12 }, 0.25);
|
|
60
83
|
}
|
|
61
|
-
|
|
84
|
+
intro.from(q("[data-opener-copy] > *"), { y: 32, autoAlpha: 0, duration: 1, ease: "expo.out", stagger: 0.1 }, 0.7);
|
|
85
|
+
// The poster line rises line by line through its masks, cut again when the width or the face changes (a cut
|
|
86
|
+
// after the entrance carries its progress over, so it never replays).
|
|
87
|
+
let lines: gsap.core.Timeline | null = null;
|
|
88
|
+
const cut = title
|
|
89
|
+
? SplitText.create(title, {
|
|
90
|
+
type: "lines",
|
|
91
|
+
mask: "lines",
|
|
92
|
+
linesClass: "struq-line",
|
|
93
|
+
autoSplit: true,
|
|
94
|
+
onSplit(self) {
|
|
95
|
+
lines = gsap
|
|
96
|
+
.timeline({ paused: !uncovered })
|
|
97
|
+
.from(self.lines, { yPercent: 115, duration: 1.25, ease: "expo.out", stagger: 0.09 }, 0.15);
|
|
98
|
+
return lines;
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
: null;
|
|
102
|
+
if (title) title.dataset.ready = "1";
|
|
103
|
+
const stop = whenUncovered(() => {
|
|
104
|
+
uncovered = true;
|
|
105
|
+
intro.play();
|
|
106
|
+
lines?.play();
|
|
107
|
+
});
|
|
108
|
+
return () => {
|
|
109
|
+
stop();
|
|
110
|
+
cut?.revert();
|
|
111
|
+
};
|
|
62
112
|
});
|
|
63
113
|
mm.add("(min-width: 768px) and (prefers-reduced-motion: no-preference)", () => {
|
|
64
114
|
// The handover: the opener holds, then scales down and darkens under the next section.
|
|
@@ -75,7 +125,9 @@ export function HeroOpener({ image, title, accent, split, lead, cta, header, scr
|
|
|
75
125
|
|
|
76
126
|
// At poster size the words need air of their own: tight tracking pulls the letters together, and
|
|
77
127
|
// word spacing in em keeps the gap in step with the size for any face.
|
|
78
|
-
|
|
128
|
+
// The size sits on each word's mask too: the shared mask's room for accents and descenders is in its own em.
|
|
129
|
+
const size = "text-[19vw] md:text-[16vw]";
|
|
130
|
+
const poster = `font-display block ${size} leading-[0.92] tracking-[-0.045em] [word-spacing:0.09em]`;
|
|
79
131
|
return (
|
|
80
132
|
<section ref={stage} data-showpiece="hero-opener" className={`relative h-svh overflow-hidden ${image ? "bg-black text-on-photo" : "scene-inverse"} ${className}`}>
|
|
81
133
|
<div data-opener-inner="" className="absolute inset-0 origin-top">
|
|
@@ -84,9 +136,9 @@ export function HeroOpener({ image, title, accent, split, lead, cta, header, scr
|
|
|
84
136
|
<div data-opener-dim="" aria-hidden className="absolute inset-0 bg-black opacity-0" />
|
|
85
137
|
{header ? <div className="absolute inset-x-0 top-0 z-20">{header}</div> : null}
|
|
86
138
|
{split ? (
|
|
87
|
-
<h1 aria-label={split.join(" ")} className="absolute inset-x-0 top-1/2 z-10 flex -translate-y-1/2 justify-between px-
|
|
139
|
+
<h1 aria-label={split.join(" ")} className="absolute inset-x-0 top-1/2 z-10 flex -translate-y-1/2 justify-between px-sheet-margin">
|
|
88
140
|
{split.map((word) => (
|
|
89
|
-
<span key={word} className=
|
|
141
|
+
<span key={word} className={`struq-line-mask block ${size}`}>
|
|
90
142
|
<span data-split-word="" data-quiet="" className={poster}>
|
|
91
143
|
{word}
|
|
92
144
|
</span>
|
|
@@ -94,7 +146,7 @@ export function HeroOpener({ image, title, accent, split, lead, cta, header, scr
|
|
|
94
146
|
))}
|
|
95
147
|
</h1>
|
|
96
148
|
) : null}
|
|
97
|
-
<div className="absolute inset-x-0 bottom-0 z-10 flex flex-col gap-[3vh] px-
|
|
149
|
+
<div className="absolute inset-x-0 bottom-0 z-10 flex flex-col gap-[3vh] px-sheet-margin pb-[3vh]">
|
|
98
150
|
<div data-opener-copy="" className={`flex flex-col gap-6 md:flex-row md:items-end md:justify-between ${side === "right" ? "md:flex-row-reverse md:text-right" : ""}`}>
|
|
99
151
|
{side === "none" ? (
|
|
100
152
|
<p data-quiet="" className="relative isolate max-w-[30ch] text-h3">
|
|
@@ -111,15 +163,22 @@ export function HeroOpener({ image, title, accent, split, lead, cta, header, scr
|
|
|
111
163
|
</div>
|
|
112
164
|
</div>
|
|
113
165
|
{!split && title ? (
|
|
114
|
-
<
|
|
115
|
-
{title}
|
|
116
|
-
{
|
|
166
|
+
<h1 data-opener-title="" data-split="" data-quiet="" className={poster}>
|
|
167
|
+
{/* An accent the title holds is marked in place; any other accent is the trailing word, as it always was. */}
|
|
168
|
+
{inTitle ? (
|
|
169
|
+
withAccent(title, accent)
|
|
170
|
+
) : (
|
|
117
171
|
<>
|
|
118
|
-
{
|
|
119
|
-
|
|
172
|
+
{title}
|
|
173
|
+
{accent ? (
|
|
174
|
+
<>
|
|
175
|
+
{" "}
|
|
176
|
+
<em className="font-accent font-light italic tracking-[-0.02em] [word-spacing:0.09em]">{accent}</em>
|
|
177
|
+
</>
|
|
178
|
+
) : null}
|
|
120
179
|
</>
|
|
121
|
-
)
|
|
122
|
-
</
|
|
180
|
+
)}
|
|
181
|
+
</h1>
|
|
123
182
|
) : null}
|
|
124
183
|
</div>
|
|
125
184
|
</div>
|
|
@@ -69,6 +69,8 @@ export function WordmarkFinale({ name, line, place, timeZone, children, classNam
|
|
|
69
69
|
const split = SplitText.create(el, {
|
|
70
70
|
type: "chars",
|
|
71
71
|
mask: "chars",
|
|
72
|
+
// The shared mask (blocks.css): room for accents and descenders in every face.
|
|
73
|
+
charsClass: "struq-char",
|
|
72
74
|
onSplit(self) {
|
|
73
75
|
return gsap.from(self.chars, {
|
|
74
76
|
yPercent: 105,
|
|
@@ -87,7 +89,7 @@ export function WordmarkFinale({ name, line, place, timeZone, children, classNam
|
|
|
87
89
|
);
|
|
88
90
|
|
|
89
91
|
return (
|
|
90
|
-
<footer ref={root} data-showpiece="wordmark-finale" className={`overflow-hidden bg-surface-0 px-
|
|
92
|
+
<footer ref={root} data-showpiece="wordmark-finale" className={`overflow-hidden bg-surface-0 px-sheet-margin pb-6 pt-24 ${className}`}>
|
|
91
93
|
<div className="flex flex-col gap-6 border-t border-line pt-6 md:flex-row md:items-end md:justify-between">
|
|
92
94
|
{line ? <p className="font-display text-h3 max-w-[24ch]">{line}</p> : <span />}
|
|
93
95
|
{children}
|
|
@@ -465,15 +465,14 @@ export function ZoomThrough({
|
|
|
465
465
|
>
|
|
466
466
|
<h2 className={`font-display ${size} leading-[0.88] tracking-[-0.04em] [word-spacing:0.08em]`}>
|
|
467
467
|
{heading.map((h) => (
|
|
468
|
-
<span
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
motion ? "overflow-hidden pb-[0.1em] -mb-[0.1em] group-data-[place=ground]/zt:pb-0 group-data-[place=ground]/zt:mb-0" : ""
|
|
472
|
-
}`}
|
|
473
|
-
>
|
|
468
|
+
<span key={h} className={`-mx-6 flex px-6 md:-mx-[5vw] md:px-[5vw] ${headRight ? "justify-end" : "justify-start"}`}>
|
|
469
|
+
{/* The ground band sits outside the shared line mask, so the mask's child paints no plate and a band
|
|
470
|
+
never grows into the line above it. */}
|
|
474
471
|
<span className={`block group-data-[place=ground]/zt:bg-surface-0 group-data-[place=ground]/zt:pb-[0.12em] group-data-[place=ground]/zt:pt-[0.08em] ${band}`}>
|
|
475
|
-
<span
|
|
476
|
-
{
|
|
472
|
+
<span className={`block ${motion ? "struq-line-mask" : ""}`}>
|
|
473
|
+
<span data-rise={motion ? "" : undefined} data-hline="" className="block whitespace-nowrap">
|
|
474
|
+
{h}
|
|
475
|
+
</span>
|
|
477
476
|
</span>
|
|
478
477
|
</span>
|
|
479
478
|
</span>
|
|
@@ -488,14 +487,14 @@ export function ZoomThrough({
|
|
|
488
487
|
}`}
|
|
489
488
|
>
|
|
490
489
|
{line ? (
|
|
491
|
-
<span className={`block max-w-[38ch] ${motion ? "
|
|
490
|
+
<span className={`block max-w-[38ch] ${motion ? "struq-line-mask" : ""}`}>
|
|
492
491
|
<span data-after={motion ? "" : undefined} className="block text-balance text-lead text-ink">
|
|
493
492
|
{line}
|
|
494
493
|
</span>
|
|
495
494
|
</span>
|
|
496
495
|
) : null}
|
|
497
496
|
{caption ? (
|
|
498
|
-
<span className={`block ${motion ? "
|
|
497
|
+
<span className={`block ${motion ? "struq-line-mask" : ""}`}>
|
|
499
498
|
<span data-after={motion ? "" : undefined} className="block text-small text-ink-muted">
|
|
500
499
|
{caption}
|
|
501
500
|
</span>
|
|
@@ -56,6 +56,10 @@ catalogue does not have: your code has to hold the same bar or it shows as the s
|
|
|
56
56
|
`document.fonts.ready` with the loaded face, and refitted on `loadingdone`. Never fit with the fallback face, and
|
|
57
57
|
never ask Google for a weight the family does not ship: `canon.mjs type` requests each face's real range.
|
|
58
58
|
- **Word spacing at poster size:** 0.06 to 0.1em on display lines, so tight tracking never runs two words into one.
|
|
59
|
+
- **Descenders are never cut.** A mask that hides a rising line must leave room below the baseline for g, j, p and y
|
|
60
|
+
and above the cap line for accents: use `SplitHeadline` or the `.struq-line-mask` class, never `overflow-hidden`
|
|
61
|
+
on a heading. At poster size keep the leading at 1 or more in a serif and 0.95 or more in a sans; look at a line
|
|
62
|
+
with a g and a j in it before you call the type done.
|
|
59
63
|
- One italic accent face at most, on one word of the page.
|
|
60
64
|
- One fluid root-rem knob (`html { font-size: clamp(...) }` in globals.css): author in rem; breakpoints change the
|
|
61
65
|
layout, never the size.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# The film: treatments, worked
|
|
2
|
+
|
|
3
|
+
SKILL.md holds the rules: the treatment template, the three languages, their arcs and the budgets. This file shows
|
|
4
|
+
them filled in. Read it once before you write your first treatment.
|
|
5
|
+
|
|
6
|
+
## The words
|
|
7
|
+
|
|
8
|
+
**Treatment.** The head of STRUQ.md: the idea, the language, the motif, the lexicon, the centerpiece and the
|
|
9
|
+
storyboard. It is the script of the site. Every block you install answers to a line of it; a block no line asks for
|
|
10
|
+
does not go on the page.
|
|
11
|
+
|
|
12
|
+
**Idea.** A point of view, not a description. "Duin is a small hotel behind the dunes" describes the client and
|
|
13
|
+
gives the site nothing to do. "Hier hoor je de zee voordat je haar ziet" is an idea: it tells you what the opening
|
|
14
|
+
withholds and what the centerpiece gives. Test it: if the sentence could open the client's Wikipedia page, it is a
|
|
15
|
+
description.
|
|
16
|
+
|
|
17
|
+
**Language.** How the whole site moves: the preloader, the page transition and the menu come from one family, so
|
|
18
|
+
every cut between scenes is made the same way. weight is slow and photographic, cut is hard and short, curtain parts
|
|
19
|
+
and holds. Pick it by the client's world, not by the block you like best.
|
|
20
|
+
|
|
21
|
+
**Motif.** One visual device from a closed set (line, frame, aperture, cut, word, field) that stands for something in
|
|
22
|
+
the client's world: the window frame of a canal house, the seam of a knitted garment, the horizon over the sea at
|
|
23
|
+
night. It shows at four places (arrival, opening, centerpiece, close) and changes a little each time, so the visitor
|
|
24
|
+
feels the page is about one thing.
|
|
25
|
+
|
|
26
|
+
**Lexicon.** Three to five working words of the client's trade: a material, a part, a tool, a unit. Not the brief
|
|
27
|
+
repeated: "twaalf kamers, Bergen aan Zee" are facts for the copy, while "helmgras, zout, vloed, sleutel" is a
|
|
28
|
+
lexicon for a hotel behind the dunes. Every label, counter, caption and margin note on the site is written with
|
|
29
|
+
them. A caption names the client's material, never "Scene 01". Trade words are not facts, so they need no source;
|
|
30
|
+
numbers still come from the brief only.
|
|
31
|
+
|
|
32
|
+
The three treatments below are examples of the form. Their palettes, pairings, packs and centerpieces belong to
|
|
33
|
+
those clients: pick yours from the canon and the arc by your own brief, and pick the language by the client's
|
|
34
|
+
world (a fashion label is cut, not weight, unless the brief says slow).
|
|
35
|
+
|
|
36
|
+
**Arc.** The order of scenes for a language, with the loud and quiet beats already placed. You cast the brief into
|
|
37
|
+
it: each scene gets this client's content. You may swap one scene for another with the same tag; the loud scenes are
|
|
38
|
+
the opening, the centerpiece and at most one gentle one, never side by side.
|
|
39
|
+
|
|
40
|
+
## How a motif travels
|
|
41
|
+
|
|
42
|
+
| Motif | Arrival | Opening | Centerpiece | Close |
|
|
43
|
+
|---|---|---|---|---|
|
|
44
|
+
| line | the cover opens on a hairline | the headline stands on a horizon or seam | a rule or slab is drawn through the words | the rule draws under the closing link |
|
|
45
|
+
| frame | the small print of the preloader | the print on the wall | the grid of prints, one opening into a room | an empty hairline frame round the last line |
|
|
46
|
+
| aperture | the cover opens through a slit | a window cut in the type | the window the camera pushes through | the footer uncovered through a growing slit |
|
|
47
|
+
| cut | the slices drop away | a slit through the poster type | a hard seam between two photos | the page ends on a cut, no fade |
|
|
48
|
+
| word | the name set across the screen | one word at poster scale | a word that fills, splits or shears | the name again, edge to edge |
|
|
49
|
+
| field | the cover in the field colour | a live field behind the type | the one field scene | the field returning under the footer |
|
|
50
|
+
|
|
51
|
+
## Quiet scenes, from the kit
|
|
52
|
+
|
|
53
|
+
The quiet scenes come from `@/components/struq/kit/quiet`, already on the page grid and the sheet's margin, each
|
|
54
|
+
entering once and then holding still: no pin, no scrub, no parallax, no hover, no counter.
|
|
55
|
+
|
|
56
|
+
- `Statement` sets the one sentence at moment size, one `accent` word in the italic face, a `label` in the margin
|
|
57
|
+
("Werkplaats Zuid, sinds 1998") and a small upright `image` hanging off the last line.
|
|
58
|
+
- `PhotoText` puts a photo across seven columns and a short heading with one or two paragraphs across four, the
|
|
59
|
+
caption under the photo in the lexicon ("Trap, schaal 1:20").
|
|
60
|
+
- `Rows` lists three to six terms with one sentence each between hairlines, a column photo beside them, no numbers.
|
|
61
|
+
- `Close` ends on one line and one link; `frame` draws the hairline frame when the motif is the frame.
|
|
62
|
+
|
|
63
|
+
Quiet scenes are what make the centerpiece read as the moment. Write an own scene only when none of the four fits,
|
|
64
|
+
and put `data-scene="<name>"` on its root.
|
|
65
|
+
|
|
66
|
+
## weight: Atelier Sloot, a ceramics studio
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
# STRUQ.md: Atelier Sloot
|
|
70
|
+
Brief: keramiekatelier in Deventer dat servies in kleine series draait, voor restaurants en voor thuis
|
|
71
|
+
Idea: Een kom is pas af als hij gebruikt wordt.
|
|
72
|
+
Language: weight
|
|
73
|
+
Motif: frame: one object alone on its plank; a small upright print at arrival, the print on the wall at the opening, the object grown to the whole screen at the centerpiece, an empty frame round the last line
|
|
74
|
+
Lexicon: klei, glazuur, oven, serie
|
|
75
|
+
Palette: <yours, from canon.mjs list by the brief's mood and sector>
|
|
76
|
+
Type: <yours, by the client's temperament>
|
|
77
|
+
Images: <the client's photos, else the pack images.mjs picks for the brief>
|
|
78
|
+
Centerpiece: image-to-fullbleed: one bowl grows from a print on the wall to the size of the screen; you see it the way you would hold it, which is the idea
|
|
79
|
+
Tone arc: paper, paper, paper, paper, paper, ink, paper, paper
|
|
80
|
+
Storyboard:
|
|
81
|
+
1. Arrival (frame) preloader-stack, "Atelier Sloot" under the frame | lands on: the hero print
|
|
82
|
+
2. Opening (loud) hero-framed:wand | on screen: one bowl as a small print, its label "Kom, mat glazuur", the h1 "Gemaakt om te gebruiken." | enters: the line rises beside the print | hands over: the first scroll walks up to the print until it fills the screen
|
|
83
|
+
3. Statement (quiet) Statement, accent "hand" | on screen: "Eerst de hand, dan de oven." at moment size on paper, the label "Klei, nog nat" in the margin, a small print of a bowl drying off the last line | enters: lines rise once | hands over: plain scroll
|
|
84
|
+
4. About (loud) about-studio:wandeling | on screen: who makes it and where, as the brief says, beside two columns of objects | enters: the columns drift with the scroll | hands over: plain scroll
|
|
85
|
+
5. Approach (quiet) Rows | on screen: "Hoe een kom ontstaat.", then draaien, drogen, glazuren, stoken, one sentence each, no numbers, a column photo of the oven beside them | enters: each row once | hands over: plain scroll
|
|
86
|
+
6. Centerpiece (loud) image-to-fullbleed | on screen: one bowl on stone, from print to full screen, with "Op ware grootte." | enters: pinned 2.2 screens | hands over: the photo lets go and scrolls away whole
|
|
87
|
+
7. Release (quiet) PhotoText | on screen: a photo of the drying rack, "Een serie voor je zaak." and two sentences on ordering one, the caption "Droogrek, één serie" | enters: once | hands over: plain scroll
|
|
88
|
+
8. Close (quiet) Close, frame | echoes the opening: an empty hairline frame round "Vraag een serie aan." and the mail link | footer: wordmark-finale
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Budgets: pins 1.5 + 2.2 = 3.7 screens; full-bleed reveals two, built the same way (a print growing to the screen);
|
|
92
|
+
clocks none, so no `timeZone` on the finale; four of the seven scenes after the arrival quiet, and the one gentle
|
|
93
|
+
loud scene (the walk past the objects) has a quiet scene on each side. `approach:draad` was left out: it draws a
|
|
94
|
+
line, and this site's motif is a frame.
|
|
95
|
+
|
|
96
|
+
## cut: Vaar, a knitwear label
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
# STRUQ.md: Vaar
|
|
100
|
+
Brief: kleine oplagen merinotruien uit een Amsterdams atelier, voor mensen die liever één goede trui hebben dan vijf
|
|
101
|
+
Idea: Eén naad, en die zit waar je hem ziet.
|
|
102
|
+
Language: cut
|
|
103
|
+
Motif: cut: the seam of a knitted garment; the slices at arrival, a slit through the poster type at the opening, the hard seam between two photos at the centerpiece, the page ending on a cut
|
|
104
|
+
Lexicon: naad, steek, merino, oplage
|
|
105
|
+
Palette: <yours, from canon.mjs list by the brief's mood and sector>
|
|
106
|
+
Type: <yours, by the client's temperament>
|
|
107
|
+
Images: <the client's photos, else the pack images.mjs picks for the brief>
|
|
108
|
+
Centerpiece: split-pair: two garments meet on one seam and the word "Naad" shears along it; the idea made physical
|
|
109
|
+
Tone arc: paper, paper, paper, paper, ink, paper, paper
|
|
110
|
+
Storyboard:
|
|
111
|
+
1. Arrival (frame) preloader-slices, name "Vaar", caption "Merino, kleine oplage, Amsterdam" | lands on: the poster
|
|
112
|
+
2. Opening (loud) hero-poster:column | on screen: "Langzaam / gebreid" fitted to the width, a slit of folded wool through it | enters: the slices uncover it | hands over: the slit opens sideways into the full photo
|
|
113
|
+
3. Work (quiet) hover-list | on screen: the pieces of this oplage, name and colour | enters: rows rise once | hands over: plain scroll
|
|
114
|
+
4. Statement (quiet) Statement, accent "naad" | on screen: "Elke trui heeft één naad." at moment size, the label "Merino, vier draden" in the margin, a small photo of the seam off the last line | enters: once | hands over: plain scroll
|
|
115
|
+
5. Centerpiece (loud) split-pair | on screen: the word "Naad", two photos meeting on it | enters: pinned 3 screens | hands over: the halves leave at their own speeds
|
|
116
|
+
6. Release (quiet) fill-rows | on screen: the three yarns, a detail on hover | enters: once | hands over: a hard cut to paper
|
|
117
|
+
7. Close (quiet) Close | echoes the opening: "Uit deze oplage, zolang hij er is." at moment size, a link to the shop at the far end of the line under it | footer: footer-contact:address
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Budgets: pins 1.5 + 3 = 4.5; one full-bleed reveal; no clock, so the footer goes without `clock`; no marquee, so
|
|
121
|
+
the close is type only.
|
|
122
|
+
|
|
123
|
+
## curtain: Nachtwerk, a night festival
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
# STRUQ.md: Nachtwerk
|
|
127
|
+
Brief: driedaags kunstfestival op de boulevard van Vlissingen, alleen na zonsondergang, voor wie de stad bij nacht wil zien
|
|
128
|
+
Idea: De stad begint pas als het donker is.
|
|
129
|
+
Language: curtain
|
|
130
|
+
Motif: line: the horizon over the water at night; the name parts on it at arrival, a low band of light on it at the opening, a slab that becomes the rising line at the centerpiece, the rule under the closing link
|
|
131
|
+
Lexicon: eb, vloed, de pier, drie nachten
|
|
132
|
+
Palette: <yours, from canon.mjs list by the brief's mood and sector>
|
|
133
|
+
Type: <yours, by the client's temperament>
|
|
134
|
+
Images: <the client's photos, else the pack images.mjs picks for the brief>
|
|
135
|
+
Centerpiece: scrub-connector: "Eb" and "Vloed" far apart, joined by one slab that seats as a rising line; the tide as the festival's clock
|
|
136
|
+
Tone arc: field, field, paper, paper, paper, ink, paper, paper
|
|
137
|
+
Storyboard:
|
|
138
|
+
1. Arrival (frame) preloader-wordmark:split, "Nachtwerk" | lands on: the field, the halves parting on its middle line
|
|
139
|
+
2. Opening (loud) background-aurora:horizon | on screen: "Drie nachten aan zee." at 16vw on the calm side, a low band of light along the foot | enters: lines rise as the band brightens | hands over: the band rises as the hero scrolls away
|
|
140
|
+
3. Statement (quiet) Statement, accent "donker" | on screen: one sentence on why the festival starts at dusk, the label "De pier, na zonsondergang" in the margin, a small photo of an empty stage off the last line | enters: once | hands over: plain scroll
|
|
141
|
+
4. Details (loud) sticky-chapters | on screen: the three nights as chapters, with the places the brief names | enters: one photo holds while the nights scroll past, each night's photo rising over the last | hands over: plain scroll
|
|
142
|
+
5. Build (quiet) PhotoText | on screen: who makes it, as the brief says, a photo of the pier, the caption "De pier bij vloed" | enters: once | hands over: plain scroll
|
|
143
|
+
6. Centerpiece (loud) scrub-connector | on screen: "Eb" and "Vloed" | enters: pinned 3 screens | hands over: the joined pair scrolls away whole
|
|
144
|
+
7. Release (quiet) PhotoText, side right | on screen: one photo of the boulevard, the dates and place from the brief | enters: once | hands over: plain scroll
|
|
145
|
+
8. Close (quiet) cta-closing:pointer | echoes the opening: the line drawn under "Kom kijken als het donker wordt." | footer: footer-reveal:lift
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Budgets: pins 3 screens; no full-bleed reveal; no clock; two contrast scenes (the field and the ink); the chapters
|
|
149
|
+
are the one gentle loud scene, a quiet scene on each side. `preloader-gate`, with its neutral hairline ring, fits
|
|
150
|
+
the language as well; its film leader (`dial="leader"`, counting 3, 2, 1) stays out, because this client is not a
|
|
151
|
+
film.
|
|
152
|
+
|
|
153
|
+
## Before the stop
|
|
154
|
+
|
|
155
|
+
Read the storyboard once as a visitor would. If two scenes in a row move with the scroll, one becomes quiet. If a
|
|
156
|
+
scene carries a metaphor from outside the client's world (a film leader, a slate, a typewriter, a clock nobody asked
|
|
157
|
+
for), it goes or its words come from the lexicon. If the centerpiece could sit on any other client's site, the idea
|
|
158
|
+
is not in it yet.
|
|
@@ -9,6 +9,9 @@ The principle: **fix the execution, preserve the identity.** Type, spacing, surf
|
|
|
9
9
|
motion, scaling, and icon misuse are execution, so they are fair game. Brand color, words,
|
|
10
10
|
and the component foundation are identity, so leave them unless explicitly asked.
|
|
11
11
|
|
|
12
|
+
A redesign writes a treatment too, with the kept sections as own scenes, and stops once for the user's approval as a
|
|
13
|
+
build does: no block and no canon goes in before the reply is logged.
|
|
14
|
+
|
|
12
15
|
---
|
|
13
16
|
|
|
14
17
|
## Preserve by default: do NOT touch unless the user asks
|
|
@@ -4,21 +4,34 @@
|
|
|
4
4
|
//
|
|
5
5
|
// node <skill-dir>/scripts/blocks.mjs list [kind] the catalogue, one line per block with its variants
|
|
6
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>
|
|
7
|
+
// node <skill-dir>/scripts/blocks.mjs add <slug>[:<variant>] [...] a block and every file it imports, plus its packages
|
|
8
|
+
// node <skill-dir>/scripts/blocks.mjs arc [language] a language's storyboard skeleton (film.json); none: the three
|
|
8
9
|
// flags: --dir <project>, --force (replace files you already have), --no-install
|
|
9
10
|
//
|
|
11
|
+
// add refuses, installing nothing, until STRUQ.md passes the plan check and STRUQ-LOG.md holds the user's reply to the
|
|
12
|
+
// treatment, and then takes only blocks the storyboard names, in the site's language (gate.mjs). list, init and arc
|
|
13
|
+
// never ask. The kit's quiet scenes (Statement, PhotoText, Rows, Close) come with init, so add only says how to import
|
|
14
|
+
// one.
|
|
15
|
+
//
|
|
10
16
|
// Everything lands under components/struq/ in the layout of Struq's blocks/ (kit/, heroes/, sections/ ...), so the
|
|
11
17
|
// relative imports between them hold. A layout or stylesheet you wrote yourself is never overwritten.
|
|
12
18
|
import { spawnSync } from "node:child_process";
|
|
13
19
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
14
20
|
import { dirname, join, relative, resolve } from "node:path";
|
|
15
21
|
import { fileURLToPath } from "node:url";
|
|
22
|
+
import { addRefusal, refuse } from "./gate.mjs";
|
|
16
23
|
import { appendLog } from "./log.mjs";
|
|
17
24
|
|
|
18
25
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
19
26
|
const BLOCKS = join(here, "..", "blocks");
|
|
20
27
|
const RECIPES = join(here, "..", "recipes");
|
|
21
28
|
const CATALOG = JSON.parse(readFileSync(join(BLOCKS, "catalog.json"), "utf8")).blocks;
|
|
29
|
+
const FILM = join(BLOCKS, "film.json");
|
|
30
|
+
/** The kit's composed quiet scenes, one file each in kit/quiet/: a storyboard names them where a block would stand. */
|
|
31
|
+
const KIT_SCENES = readdirSync(join(BLOCKS, "kit", "quiet"))
|
|
32
|
+
.filter((n) => /^[A-Z]\w*\.tsx$/.test(n))
|
|
33
|
+
.map((n) => n.replace(/\.tsx$/, ""));
|
|
34
|
+
const kitScene = (name) => KIT_SCENES.find((k) => k.toLowerCase() === name.split(":")[0].toLowerCase()) ?? null;
|
|
22
35
|
|
|
23
36
|
const args = process.argv.slice(2);
|
|
24
37
|
const dirAt = args.indexOf("--dir");
|
|
@@ -83,15 +96,47 @@ function report(step, what) {
|
|
|
83
96
|
appendLog(root, step, [what, ...(notes.length ? [`Notes: ${notes.join(" ")}`] : [])]);
|
|
84
97
|
}
|
|
85
98
|
|
|
99
|
+
/**
|
|
100
|
+
* A language's arc as STRUQ.md lines: the Tone arc, then one numbered Storyboard line per scene in the treatment's
|
|
101
|
+
* format, the default pick filled in (the own scene first where the arc prefers it, as its kit quiet scene when it has
|
|
102
|
+
* one, else the first block) and what else fits after `or:`. The defaults pass check.mjs --plan as they stand.
|
|
103
|
+
*/
|
|
104
|
+
function arcLines(arc) {
|
|
105
|
+
const pick = (scene) => {
|
|
106
|
+
const own = scene.kit ?? "own scene";
|
|
107
|
+
const ownFirst = scene.own && (scene.ownFirst || !scene.blocks.length);
|
|
108
|
+
const options = ownFirst ? [own, ...scene.blocks] : [...scene.blocks, ...(scene.own ? [own] : [])];
|
|
109
|
+
if (scene.kit && scene.own && !options.includes("own scene")) options.push("own scene");
|
|
110
|
+
return options;
|
|
111
|
+
};
|
|
112
|
+
const width = Math.max(...arc.scenes.map((s) => s.name.length));
|
|
113
|
+
const last = arc.scenes.length - 1;
|
|
114
|
+
const lines = [`Tone arc: ${arc.scenes.map((s) => s.tone[0]).join(", ")}`, "Storyboard:"];
|
|
115
|
+
arc.scenes.forEach((scene, at) => {
|
|
116
|
+
const [first, ...others] = pick(scene);
|
|
117
|
+
const head = `${`${at + 1}.`.padEnd(at + 1 >= 10 ? 4 : 3)}${scene.name.padEnd(width)} ${`(${scene.tag})`.padEnd(8)} ${first}`;
|
|
118
|
+
const parts = [head];
|
|
119
|
+
if (scene.tag === "frame") parts.push("lands on: <what>");
|
|
120
|
+
else if (at === last) parts.push("echoes the opening: <how>");
|
|
121
|
+
else parts.push("on screen: <...>", "enters: <...>", "hands over: <...>");
|
|
122
|
+
if (others.length) parts.push(`or: ${others.join(", ")}`);
|
|
123
|
+
if (scene.footer?.length) parts.push(`footer: ${scene.footer[0]}`);
|
|
124
|
+
lines.push(parts.join(" | "));
|
|
125
|
+
});
|
|
126
|
+
return lines;
|
|
127
|
+
}
|
|
128
|
+
|
|
86
129
|
const commands = {
|
|
87
130
|
list() {
|
|
88
131
|
const kind = rest[0];
|
|
89
132
|
const rows = CATALOG.filter((b) => !kind || b.kind === kind);
|
|
90
133
|
if (!rows.length) throw new Error(`No blocks of kind "${kind}". Kinds: ${[...new Set(CATALOG.map((b) => b.kind))].join(", ")}`);
|
|
134
|
+
// The film facts of the default variant, so a storyboard can be cast from this list alone.
|
|
135
|
+
const film = (b) => (b.role ? ` film: ${b.role} · energy ${b.energy ?? 0} · pins ${b.pins ? b.scroll : 0}` : "");
|
|
91
136
|
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(", ")}]` : ""}`);
|
|
137
|
+
console.log(`${b.kind.padEnd(16)} ${b.slug.padEnd(24)} ${b.title}${b.variants.length ? ` [${b.variants.map((v) => v.id).join(", ")}]` : ""}${film(b)}`);
|
|
93
138
|
}
|
|
94
|
-
console.log(`\n${rows.length} block(s). What each does and
|
|
139
|
+
console.log(`\n${rows.length} block(s). What each does, its variants and their film lines: blocks/CATALOG.md.`);
|
|
95
140
|
},
|
|
96
141
|
|
|
97
142
|
init() {
|
|
@@ -120,14 +165,51 @@ const commands = {
|
|
|
120
165
|
console.log("OK init ready");
|
|
121
166
|
},
|
|
122
167
|
|
|
168
|
+
arc() {
|
|
169
|
+
const film = JSON.parse(readFileSync(FILM, "utf8"));
|
|
170
|
+
const languages = film.FILM_LANGUAGES;
|
|
171
|
+
const id = rest[0];
|
|
172
|
+
if (!id) {
|
|
173
|
+
const width = Math.max(...languages.map((l) => l.id.length)) + 2;
|
|
174
|
+
for (const l of languages) console.log(`${l.id.padEnd(width)}${l.for.join(", ")}`);
|
|
175
|
+
console.log(`\nPrint one: blocks.mjs arc <${languages.map((l) => l.id).join(" | ")}>`);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const language = languages.find((l) => l.id === id);
|
|
179
|
+
const arc = film.FILM_ARCS.find((a) => a.language === id);
|
|
180
|
+
if (!language || !arc) throw new Error(`No language "${id}". The languages: ${languages.map((l) => l.id).join(", ")}.`);
|
|
181
|
+
for (const line of arcLines(arc)) console.log(line);
|
|
182
|
+
const f = language.frames;
|
|
183
|
+
console.log(`\nFrames of ${id}: preloader ${f.preloader}, page transition ${f.pageTransition} (only with a second route), menu ${f.menu}. Header: ${film.FILM_HEADER.join(" or ")}.`);
|
|
184
|
+
console.log(`Use: paste the Tone arc and the Storyboard into STRUQ.md under your treatment head, fill every <...>, keep one pick per line (or: lists what else fits), then run check.mjs --plan.`);
|
|
185
|
+
},
|
|
186
|
+
|
|
123
187
|
add() {
|
|
124
|
-
if (!rest.length) throw new Error("Usage: blocks.mjs add <slug>
|
|
188
|
+
if (!rest.length) throw new Error("Usage: blocks.mjs add <slug>[:<variant>] [...]. The slugs: blocks.mjs list");
|
|
189
|
+
// A kit quiet scene is no block: init already copied it, so say how to import it and install nothing for it.
|
|
190
|
+
for (const arg of rest.filter(kitScene)) {
|
|
191
|
+
const name = kitScene(arg);
|
|
192
|
+
console.log(`${name} is a kit quiet scene, already in components/struq/kit/quiet/ (blocks.mjs init): import { ${name} } from "@/components/struq/kit/quiet";`);
|
|
193
|
+
}
|
|
194
|
+
rest.splice(0, rest.length, ...rest.filter((arg) => !kitScene(arg)));
|
|
195
|
+
if (!rest.length) return;
|
|
196
|
+
const requests = rest.map((arg) => {
|
|
197
|
+
const [slug, variant = null] = arg.split(":");
|
|
198
|
+
const b = CATALOG.find((x) => x.slug === slug);
|
|
199
|
+
if (!b) throw new Error(`Unknown block "${slug}". Run: blocks.mjs list`);
|
|
200
|
+
if (variant && !b.variants.some((v) => v.id === variant)) {
|
|
201
|
+
throw new Error(`${slug} has no variant "${variant}". Its variants: ${b.variants.map((v) => v.id).join(", ") || "none"}.`);
|
|
202
|
+
}
|
|
203
|
+
return { slug, variant };
|
|
204
|
+
});
|
|
125
205
|
const deps = project();
|
|
206
|
+
// The one stop: a passing plan, the user's logged reply, and only blocks the storyboard names (gate.mjs).
|
|
207
|
+
const refusal = addRefusal(root, requests);
|
|
208
|
+
if (refusal) return refuse("blocks.mjs add", refusal);
|
|
126
209
|
const wanted = new Set();
|
|
127
210
|
const added = [];
|
|
128
|
-
for (const slug of
|
|
211
|
+
for (const { slug } of requests) {
|
|
129
212
|
const b = CATALOG.find((x) => x.slug === slug);
|
|
130
|
-
if (!b) throw new Error(`Unknown block "${slug}". Run: blocks.mjs list`);
|
|
131
213
|
for (const f of b.files) place(join(BLOCKS, f), target(f));
|
|
132
214
|
for (const p of b.packages) wanted.add(p);
|
|
133
215
|
added.push(b);
|
|
@@ -145,7 +227,7 @@ const commands = {
|
|
|
145
227
|
|
|
146
228
|
const run = commands[cmd];
|
|
147
229
|
if (!run) {
|
|
148
|
-
console.error("Usage: blocks.mjs list [kind] | init | add <slug>
|
|
230
|
+
console.error("Usage: blocks.mjs list [kind] | init | add <slug>[:<variant>] [...] | arc [language] [--dir <project>] [--force] [--no-install]");
|
|
149
231
|
process.exit(2);
|
|
150
232
|
}
|
|
151
233
|
try {
|
|
@@ -9,10 +9,14 @@
|
|
|
9
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
|
+
// palette and type refuse, writing nothing, until STRUQ.md passes the plan check and STRUQ-LOG.md holds the user's
|
|
13
|
+
// reply to the treatment (gate.mjs). list and print never ask.
|
|
14
|
+
//
|
|
12
15
|
// Node 18 or later, no network, no dependencies.
|
|
13
16
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
17
|
import { dirname, join, resolve } from "node:path";
|
|
15
18
|
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { refuse, stopRefusal } from "./gate.mjs";
|
|
16
20
|
import { appendLog } from "./log.mjs";
|
|
17
21
|
import { fontHrefs, headFamily, lightness, paletteTokens } from "./tokens.mjs";
|
|
18
22
|
|
|
@@ -126,7 +130,10 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur
|
|
|
126
130
|
process.exit(2);
|
|
127
131
|
}
|
|
128
132
|
try {
|
|
129
|
-
|
|
133
|
+
// palette and type write into the project, so they wait for the one stop: a passing plan and the logged reply.
|
|
134
|
+
const refusal = cmd === "palette" || cmd === "type" ? stopRefusal(root) : null;
|
|
135
|
+
if (refusal) refuse(`canon.mjs ${cmd}, nothing written`, refusal);
|
|
136
|
+
else run();
|
|
130
137
|
} catch (e) {
|
|
131
138
|
console.error(`FAIL ${e.message}`);
|
|
132
139
|
process.exitCode = 1;
|