dreative 0.5.2 → 0.5.3
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/dist/cli/docsCheck.js +1 -1
- package/dist/shared/artifacts.js +58 -0
- package/dist/shared/design.js +3 -2
- package/dist/shared/ruleSystem.js +100 -10
- package/dist/shared/ruleSystem.test.js +94 -19
- package/package.json +1 -1
- package/skill/dreative/DESIGN.md +34 -26
- package/skill/dreative/PLAN.md +84 -52
- package/skill/dreative/SKILL.md +21 -9
- package/skill/dreative/recipes/motion-recipes.md +24 -0
- package/skill/dreative/references/ARTIFACTS.md +27 -11
- package/skill/dreative/references/RULES.json +9 -9
- package/skill/dreative/references/TIERS.md +7 -5
- package/skill/dreative/schemas/plan.schema.json +74 -6
- package/skill/dreative/schemas/verify.schema.json +14 -0
- package/skill/dreative/skills/media.md +28 -14
- package/skill/dreative/skills/mobile.md +13 -12
- package/skill/dreative/skills/motion.md +45 -8
package/dist/cli/docsCheck.js
CHANGED
|
@@ -82,7 +82,7 @@ export function runDocsCheck(skillDir) {
|
|
|
82
82
|
add(findings, "recipes", specialist, "inline recipe catalog remains in the primary specialist file");
|
|
83
83
|
}
|
|
84
84
|
const planSchema = contents.get("schemas/plan.schema.json") ?? "";
|
|
85
|
-
for (const field of ["doctrineVersion", "ruleExceptions", "creativeStrategy", "fontDecision", "experimentalPlan", "conceptExploration", "recipeAccess"]) {
|
|
85
|
+
for (const field of ["doctrineVersion", "ruleExceptions", "creativeStrategy", "motionComplexityBudget", "motionTreatment", "preparation", "fontDecision", "experimentalPlan", "conceptExploration", "recipeAccess"]) {
|
|
86
86
|
if (!planSchema.includes(`\"${field}\"`))
|
|
87
87
|
add(findings, "schema", "schemas/plan.schema.json", `missing creative-control field ${field}`);
|
|
88
88
|
}
|
package/dist/shared/artifacts.js
CHANGED
|
@@ -59,6 +59,26 @@ export function validatePlan(value) {
|
|
|
59
59
|
errors.push(`${prefix} is still planned`);
|
|
60
60
|
if ((section.status === "fallback" || section.status === "cut") && !nonEmpty(section.reason))
|
|
61
61
|
errors.push(`${prefix}.reason is required for ${section.status}`);
|
|
62
|
+
if (section.motionTreatment) {
|
|
63
|
+
const treatment = section.motionTreatment;
|
|
64
|
+
if (!["none", "decorative", "structural", "transformational"].includes(treatment.class))
|
|
65
|
+
errors.push(`${prefix}.motionTreatment.class is invalid`);
|
|
66
|
+
for (const [name, value] of Object.entries({
|
|
67
|
+
staticComposition: treatment.staticComposition,
|
|
68
|
+
startState: treatment.startState,
|
|
69
|
+
endState: treatment.endState,
|
|
70
|
+
handoff: treatment.handoff,
|
|
71
|
+
purpose: treatment.purpose,
|
|
72
|
+
mechanism: treatment.mechanism,
|
|
73
|
+
mobile: treatment.mobile,
|
|
74
|
+
reducedMotion: treatment.reducedMotion,
|
|
75
|
+
})) {
|
|
76
|
+
if (!nonEmpty(value))
|
|
77
|
+
errors.push(`${prefix}.motionTreatment.${name} is required`);
|
|
78
|
+
}
|
|
79
|
+
if (!Array.isArray(treatment.changes) || !Array.isArray(treatment.pinnedElements))
|
|
80
|
+
errors.push(`${prefix}.motionTreatment changes and pinnedElements must be arrays`);
|
|
81
|
+
}
|
|
62
82
|
for (const skill of section.skills ?? []) {
|
|
63
83
|
if (!page.skills?.includes(skill))
|
|
64
84
|
errors.push(`${prefix} uses skill ${skill} not assigned to its page`);
|
|
@@ -66,6 +86,20 @@ export function validatePlan(value) {
|
|
|
66
86
|
for (const asset of section.assets ?? []) {
|
|
67
87
|
if (!nonEmpty(asset.id) || !nonEmpty(asset.path) || !nonEmpty(asset.purpose))
|
|
68
88
|
errors.push(`${prefix} contains an incomplete asset`);
|
|
89
|
+
if (plan.doctrineVersion === 2 && (!asset.importance || !asset.preparation))
|
|
90
|
+
errors.push(`${prefix} asset ${asset.id || "unknown"} needs importance and a preparation decision`);
|
|
91
|
+
else if (asset.preparation) {
|
|
92
|
+
if (!asset.importance || !["supporting", "key"].includes(asset.importance))
|
|
93
|
+
errors.push(`${prefix} asset ${asset.id} has invalid importance`);
|
|
94
|
+
if (!["flat", "decompose", "variants", "sequence"].includes(asset.preparation.decision))
|
|
95
|
+
errors.push(`${prefix} asset ${asset.id} has an invalid preparation decision`);
|
|
96
|
+
if (!nonEmpty(asset.preparation.rationale))
|
|
97
|
+
errors.push(`${prefix} asset ${asset.id} needs a preparation rationale`);
|
|
98
|
+
if (!Array.isArray(asset.preparation.derivatives))
|
|
99
|
+
errors.push(`${prefix} asset ${asset.id} preparation.derivatives must be an array`);
|
|
100
|
+
if (asset.importance === "key" && asset.preparation.decision !== "flat" && asset.preparation.derivatives.length === 0)
|
|
101
|
+
errors.push(`${prefix} key asset ${asset.id} promises preparation but names no derivatives`);
|
|
102
|
+
}
|
|
69
103
|
if ((asset.status === "fallback" || asset.status === "cut") && !nonEmpty(asset.reason))
|
|
70
104
|
errors.push(`${prefix} asset ${asset.id} needs a reason`);
|
|
71
105
|
}
|
|
@@ -132,11 +166,35 @@ export function validateVerificationReport(value) {
|
|
|
132
166
|
errors.push("verification report version must be 1");
|
|
133
167
|
if (!Array.isArray(report.evidence))
|
|
134
168
|
errors.push("verification evidence must be an array");
|
|
169
|
+
if (report.refinement) {
|
|
170
|
+
if (!nonEmpty(report.refinement.inspectedAt) || Number.isNaN(Date.parse(report.refinement.inspectedAt)))
|
|
171
|
+
errors.push("verification refinement needs a valid inspectedAt timestamp");
|
|
172
|
+
if (!Array.isArray(report.refinement.findings) ||
|
|
173
|
+
report.refinement.findings.length === 0 ||
|
|
174
|
+
!Array.isArray(report.refinement.changes) ||
|
|
175
|
+
report.refinement.changes.length === 0 ||
|
|
176
|
+
!Array.isArray(report.refinement.evidenceIds) ||
|
|
177
|
+
report.refinement.evidenceIds.length === 0)
|
|
178
|
+
errors.push("verification refinement needs findings, changes, and evidenceIds arrays");
|
|
179
|
+
}
|
|
135
180
|
if ((report.evidence ?? []).some((item) => item.status === "fail"))
|
|
136
181
|
errors.push("verification report contains failing evidence");
|
|
182
|
+
const timelineStates = new Set([
|
|
183
|
+
"initial",
|
|
184
|
+
"early",
|
|
185
|
+
"mid-transition",
|
|
186
|
+
"final",
|
|
187
|
+
"handoff",
|
|
188
|
+
"pinned-midpoint",
|
|
189
|
+
"pinned-exit",
|
|
190
|
+
"mobile",
|
|
191
|
+
"reduced-motion",
|
|
192
|
+
]);
|
|
137
193
|
for (const [index, item] of (report.evidence ?? []).entries()) {
|
|
138
194
|
if (!nonEmpty(item.id) || !nonEmpty(item.criterion) || !nonEmpty(item.evidence))
|
|
139
195
|
errors.push(`verification.evidence[${index}] is incomplete`);
|
|
196
|
+
if (item.timelineState && !timelineStates.has(item.timelineState))
|
|
197
|
+
errors.push(`verification.evidence[${index}].timelineState is invalid`);
|
|
140
198
|
const proof = item.proof;
|
|
141
199
|
if (!proof || !nonEmpty(proof.timestamp) || Number.isNaN(Date.parse(proof.timestamp))) {
|
|
142
200
|
errors.push(`verification.evidence[${index}].proof requires a valid timestamp`);
|
package/dist/shared/design.js
CHANGED
|
@@ -142,12 +142,13 @@ export function buildDesignPlan(page, brief, assignedSkills) {
|
|
|
142
142
|
const motionBudget = dials.motion <= 3
|
|
143
143
|
? "hover/active states only, no scroll animation"
|
|
144
144
|
: dials.motion <= 6
|
|
145
|
-
? "
|
|
146
|
-
: "
|
|
145
|
+
? "purposeful local motion; use structural transitions only where content changes state, with visible defaults"
|
|
146
|
+
: "write section motion treatments; concentrate structural/transformational choreography into a few hero moments with calm sections, mobile translations, and reduced-motion states";
|
|
147
147
|
const directives = [
|
|
148
148
|
`aesthetic: ${aesthetic}; dials variance=${dials.variance} motion=${dials.motion} density=${dials.density}`,
|
|
149
149
|
`spacing scale: ${spacing}`,
|
|
150
150
|
`motion budget: ${motionBudget}`,
|
|
151
|
+
"choose the simplest mechanism that makes each intended transformation convincing; do not default blindly to fade/slide/scale or to WebGL/3D",
|
|
151
152
|
"one accent color, one neutral family, one radius system, locked across ALL pages of this project",
|
|
152
153
|
"section layout families are assigned below — follow them, do not default to repeated card rows",
|
|
153
154
|
...(dials.variance > 4 ? ["avoid centered-hero-over-gradient; use split or asymmetric composition"] : []),
|
|
@@ -9,6 +9,12 @@ function specific(value, minLength) {
|
|
|
9
9
|
return false;
|
|
10
10
|
return !/^(it |this )?(did not fit|didn't fit|felt better|was unnecessary|was not needed|looked better|fits? the design|restraint)$/i.test(value.trim());
|
|
11
11
|
}
|
|
12
|
+
function genericMotionOnly(value) {
|
|
13
|
+
const text = normalized(value);
|
|
14
|
+
const basic = /\b(fade|opacity|translate|slide|scale|zoom|parallax|reveal|stagger)\b/;
|
|
15
|
+
const structural = /\b(mask|fragment|tile|pixel|layer|depth|pin|persist|handoff|morph|reconstruct|sequence|canvas|svg|webgl|shader|state|composition|camera|clip-path|typography)\b/;
|
|
16
|
+
return basic.test(text) && !structural.test(text);
|
|
17
|
+
}
|
|
12
18
|
export function validateRuleControls(plan, registry, reflexFonts, verification) {
|
|
13
19
|
if (plan.doctrineVersion !== 2)
|
|
14
20
|
return [];
|
|
@@ -81,20 +87,104 @@ export function validateRuleControls(plan, registry, reflexFonts, verification)
|
|
|
81
87
|
if (!strategy)
|
|
82
88
|
errors.push(`${plan.tier} plans require a diversity-or-development creativeStrategy`);
|
|
83
89
|
else if (strategy.path === "diversity") {
|
|
84
|
-
if (distinct(strategy.mechanisms ?? []).length
|
|
85
|
-
errors.push("diversity path requires
|
|
86
|
-
if (distinct(strategy.drivers ?? []).length
|
|
87
|
-
errors.push("diversity path requires
|
|
90
|
+
if (distinct(strategy.mechanisms ?? []).length === 0)
|
|
91
|
+
errors.push("diversity path requires at least one concept-specific mechanism");
|
|
92
|
+
if (distinct(strategy.drivers ?? []).length === 0)
|
|
93
|
+
errors.push("diversity path requires at least one meaningful driver");
|
|
88
94
|
}
|
|
89
95
|
else {
|
|
90
96
|
if (!specific(strategy.signatureMechanism, 12))
|
|
91
97
|
errors.push("development path requires a named signature mechanism");
|
|
92
|
-
if (distinct(strategy.states ?? []).length <
|
|
93
|
-
errors.push("development path requires
|
|
94
|
-
if (
|
|
95
|
-
errors.push("development path
|
|
96
|
-
if (distinct(strategy.drivers ?? []).length
|
|
97
|
-
errors.push("development path requires
|
|
98
|
+
if (distinct(strategy.states ?? []).length < 2 || (strategy.states ?? []).some((state) => state.trim().length < 12))
|
|
99
|
+
errors.push("development path requires materially different described states");
|
|
100
|
+
if (!Array.isArray(strategy.secondaryMechanisms))
|
|
101
|
+
errors.push("development path must record its quieter supporting mechanisms");
|
|
102
|
+
if (distinct(strategy.drivers ?? []).length === 0)
|
|
103
|
+
errors.push("development path requires a meaningful driver");
|
|
104
|
+
}
|
|
105
|
+
const sections = plan.pages.flatMap((page) => page.sections);
|
|
106
|
+
const sectionIds = new Set(sections.map((section) => section.id));
|
|
107
|
+
const budget = plan.motionComplexityBudget;
|
|
108
|
+
if (!budget)
|
|
109
|
+
errors.push(`${plan.tier} plans require a contextual motionComplexityBudget`);
|
|
110
|
+
else {
|
|
111
|
+
if (budget.heroMoments.length === 0 || budget.heroMoments.length > 3)
|
|
112
|
+
errors.push("motion complexity must be concentrated into one to three hero moments");
|
|
113
|
+
for (const moment of budget.heroMoments) {
|
|
114
|
+
if (!sectionIds.has(moment.sectionId))
|
|
115
|
+
errors.push(`motion hero moment references unknown section ${moment.sectionId}`);
|
|
116
|
+
if (!specific(moment.reason, 20))
|
|
117
|
+
errors.push(`motion hero moment ${moment.sectionId} needs a concept-specific reason`);
|
|
118
|
+
}
|
|
119
|
+
for (const sectionId of budget.calmSectionIds) {
|
|
120
|
+
if (!sectionIds.has(sectionId))
|
|
121
|
+
errors.push(`motion calm section references unknown section ${sectionId}`);
|
|
122
|
+
}
|
|
123
|
+
if (!specific(budget.sharedLanguage, 20) || !specific(budget.deviceLimits, 20) || !specific(budget.progressiveEnhancement, 20))
|
|
124
|
+
errors.push("motion complexity budget needs shared language, device limits, and progressive-enhancement decisions");
|
|
125
|
+
const antiDefault = budget.antiDefaultReview;
|
|
126
|
+
if (!antiDefault)
|
|
127
|
+
errors.push("motion complexity budget requires the anti-default review");
|
|
128
|
+
else
|
|
129
|
+
for (const [name, answer] of Object.entries(antiDefault)) {
|
|
130
|
+
if (!specific(answer, 20))
|
|
131
|
+
errors.push(`anti-default review ${name} needs a concrete answer`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const treatments = sections.map((section) => section.motionTreatment);
|
|
135
|
+
if (treatments.some((treatment) => !treatment))
|
|
136
|
+
errors.push(`${plan.tier} plans require a motion treatment for every major section, including calm sections`);
|
|
137
|
+
const significant = treatments.filter((treatment) => Boolean(treatment && (treatment.class === "structural" || treatment.class === "transformational")));
|
|
138
|
+
if (significant.length === 0)
|
|
139
|
+
errors.push(`${plan.tier} motion cannot be decorative-only`);
|
|
140
|
+
if (significant.length > 0 &&
|
|
141
|
+
significant.every((treatment) => genericMotionOnly(`${treatment.mechanism} ${(treatment.changes ?? []).join(" ")} ${treatment.handoff}`)))
|
|
142
|
+
errors.push(`${plan.tier} motion still describes only generic fade/slide/scale behavior; revise the scene architecture`);
|
|
143
|
+
for (const [index, treatment] of treatments.entries()) {
|
|
144
|
+
if (!treatment)
|
|
145
|
+
continue;
|
|
146
|
+
const prefix = `motion treatment ${sections[index].id}`;
|
|
147
|
+
for (const [name, value] of Object.entries({
|
|
148
|
+
staticComposition: treatment.staticComposition,
|
|
149
|
+
startState: treatment.startState,
|
|
150
|
+
endState: treatment.endState,
|
|
151
|
+
handoff: treatment.handoff,
|
|
152
|
+
purpose: treatment.purpose,
|
|
153
|
+
mechanism: treatment.mechanism,
|
|
154
|
+
mobile: treatment.mobile,
|
|
155
|
+
reducedMotion: treatment.reducedMotion,
|
|
156
|
+
})) {
|
|
157
|
+
if (!specific(value, 12))
|
|
158
|
+
errors.push(`${prefix}.${name} needs a concrete decision`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (verification) {
|
|
162
|
+
const timelineStates = new Set(verification.evidence.map((item) => item.timelineState).filter(Boolean));
|
|
163
|
+
const requiredStates = [
|
|
164
|
+
"initial",
|
|
165
|
+
"early",
|
|
166
|
+
"mid-transition",
|
|
167
|
+
"final",
|
|
168
|
+
"handoff",
|
|
169
|
+
"mobile",
|
|
170
|
+
"reduced-motion",
|
|
171
|
+
];
|
|
172
|
+
if (treatments.some((treatment) => (treatment?.pinnedElements?.length ?? 0) > 0))
|
|
173
|
+
requiredStates.push("pinned-midpoint", "pinned-exit");
|
|
174
|
+
for (const state of requiredStates) {
|
|
175
|
+
if (!timelineStates.has(state))
|
|
176
|
+
errors.push(`visual verification is missing timeline state ${state}`);
|
|
177
|
+
}
|
|
178
|
+
const refinement = verification.refinement;
|
|
179
|
+
if (!refinement || refinement.changes.length === 0 || refinement.evidenceIds.length === 0)
|
|
180
|
+
errors.push(`${plan.tier} verification requires a recorded refinement pass after visual inspection`);
|
|
181
|
+
else {
|
|
182
|
+
for (const id of refinement.evidenceIds) {
|
|
183
|
+
const item = evidence.get(id);
|
|
184
|
+
if (!item || item.status !== "pass")
|
|
185
|
+
errors.push(`refinement references missing or failing evidence ${id}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
98
188
|
}
|
|
99
189
|
}
|
|
100
190
|
if (plan.skills.includes("experimental")) {
|
|
@@ -33,6 +33,20 @@ function basePlan(tier = "award") {
|
|
|
33
33
|
mechanisms: ["variable-type morph", "depth-stack", "drag-inertia", "mask-dissolve"],
|
|
34
34
|
drivers: ["scroll", "pointer", "time"],
|
|
35
35
|
},
|
|
36
|
+
motionComplexityBudget: {
|
|
37
|
+
heroMoments: [{ sectionId: "hero", reason: "The archival specimen becomes the navigation system and establishes the living-catalog concept." }],
|
|
38
|
+
calmSectionIds: [],
|
|
39
|
+
sharedLanguage: "Archival sheets fold, align, and become structural boundaries across the journey.",
|
|
40
|
+
deviceLimits: "Desktop uses the full pinned chapter while mobile shortens travel and removes continuous rendering.",
|
|
41
|
+
progressiveEnhancement: "Semantic catalog content renders first; masks and synchronized motion enhance it when supported.",
|
|
42
|
+
antiDefaultReview: {
|
|
43
|
+
basicMotionAssessment: "The key specimen changes role and geometry rather than only fading, sliding, or scaling.",
|
|
44
|
+
compositionHandoff: "The specimen edge becomes the next chapter's indexing rail instead of disappearing.",
|
|
45
|
+
visualStateChange: "Loose sheets become navigation, a research surface, and then the closing mark.",
|
|
46
|
+
conceptSpecificity: "The sheet behavior is derived from archival handling and would not fit an unrelated product.",
|
|
47
|
+
memorableMoment: "Dragging the specimen reorganizes the complete catalog into the next chapter.",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
36
50
|
pages: [
|
|
37
51
|
{
|
|
38
52
|
id: "home",
|
|
@@ -45,6 +59,19 @@ function basePlan(tier = "award") {
|
|
|
45
59
|
layoutFamily: "archival-index",
|
|
46
60
|
skills: ["ux", "mobile", "motion", "media"],
|
|
47
61
|
interactions: ["drag specimen"],
|
|
62
|
+
motionTreatment: {
|
|
63
|
+
class: "transformational",
|
|
64
|
+
staticComposition: "A loose archival specimen sits beside the catalog index.",
|
|
65
|
+
startState: "The specimen is centered and isolated as the page's primary subject.",
|
|
66
|
+
endState: "The specimen unfolds into the structural rail for the next catalog chapter.",
|
|
67
|
+
changes: ["The specimen mask expands and its edge becomes the next chapter rail."],
|
|
68
|
+
pinnedElements: [],
|
|
69
|
+
handoff: "The specimen edge persists as the indexing rail in the following composition.",
|
|
70
|
+
purpose: "Turn passive archival viewing into active handling and establish continuity.",
|
|
71
|
+
mechanism: "SVG mask migration synchronized with a GSAP scroll timeline.",
|
|
72
|
+
mobile: "A shorter clip-path handoff preserves the specimen-to-rail transformation.",
|
|
73
|
+
reducedMotion: "Render the specimen already aligned with the rail and use a direct section cut.",
|
|
74
|
+
},
|
|
48
75
|
mobile: "stacked specimen",
|
|
49
76
|
fallback: "static catalog",
|
|
50
77
|
verification: ["Signature responds to input"],
|
|
@@ -59,41 +86,59 @@ function basePlan(tier = "award") {
|
|
|
59
86
|
};
|
|
60
87
|
}
|
|
61
88
|
function verification(...ids) {
|
|
89
|
+
const timelineStates = ["initial", "early", "mid-transition", "final", "handoff", "mobile", "reduced-motion"];
|
|
90
|
+
const timeline = timelineStates.map((timelineState) => ({
|
|
91
|
+
id: `timeline-${timelineState}`,
|
|
92
|
+
criterion: `Captured ${timelineState} visual state`,
|
|
93
|
+
status: "pass",
|
|
94
|
+
evidence: "Captured the production build at the planned visual state.",
|
|
95
|
+
timelineState,
|
|
96
|
+
proof: { timestamp: started, testedUrl: "http://localhost:3000", consoleErrorCount: 0 },
|
|
97
|
+
}));
|
|
62
98
|
return {
|
|
63
99
|
version: 1,
|
|
64
100
|
generatedAt: started,
|
|
65
|
-
evidence:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
101
|
+
evidence: [
|
|
102
|
+
...timeline,
|
|
103
|
+
...ids.map((id) => ({
|
|
104
|
+
id,
|
|
105
|
+
criterion: id,
|
|
106
|
+
status: "pass",
|
|
107
|
+
evidence: "Captured runtime behavior in the production build.",
|
|
108
|
+
proof: { timestamp: started, testedUrl: "http://localhost:3000", consoleErrorCount: 0 },
|
|
109
|
+
})),
|
|
110
|
+
],
|
|
111
|
+
refinement: {
|
|
112
|
+
inspectedAt: started,
|
|
113
|
+
findings: ["The mid-transition index was too compressed to read."],
|
|
114
|
+
changes: ["Extended the mask handoff and delayed the rail compression."],
|
|
115
|
+
evidenceIds: ["timeline-mid-transition"],
|
|
116
|
+
},
|
|
72
117
|
};
|
|
73
118
|
}
|
|
74
119
|
test("valid default award plan", () => {
|
|
75
120
|
assert.deepEqual(validateRuleControls(basePlan(), registry, reflex, verification()), []);
|
|
76
121
|
});
|
|
77
|
-
test("valid
|
|
122
|
+
test("valid key-asset substitution for a typography-only experience", () => {
|
|
78
123
|
const plan = basePlan();
|
|
79
124
|
plan.ruleExceptions = [
|
|
80
125
|
{
|
|
81
|
-
ruleId: "
|
|
126
|
+
ruleId: "media.keyAssetTreatment",
|
|
82
127
|
decision: "substituted",
|
|
83
128
|
declaredAt: planned,
|
|
84
|
-
reason: "The
|
|
85
|
-
alternative: "A persistent variable-type specimen becomes index, spatial architecture, and interactive control across
|
|
86
|
-
successCriteria: ["The specimen appears in
|
|
87
|
-
evidenceIds: ["
|
|
129
|
+
reason: "The archive contains no raster key imagery; inventing photography would misrepresent the supplied typographic material.",
|
|
130
|
+
alternative: "A persistent variable-type specimen becomes index, spatial architecture, and interactive control across the complete journey.",
|
|
131
|
+
successCriteria: ["The specimen appears in materially different structural roles", "Pointer and scroll input visibly reshape the specimen"],
|
|
132
|
+
evidenceIds: ["type-system-desktop", "type-system-mobile"],
|
|
88
133
|
},
|
|
89
134
|
];
|
|
90
|
-
assert.deepEqual(validateRuleControls(plan, registry, reflex, verification("
|
|
135
|
+
assert.deepEqual(validateRuleControls(plan, registry, reflex, verification("type-system-desktop", "type-system-mobile")), []);
|
|
91
136
|
});
|
|
92
|
-
test("invalid vague
|
|
137
|
+
test("invalid vague creative-rule exception", () => {
|
|
93
138
|
const plan = basePlan();
|
|
94
139
|
plan.ruleExceptions = [
|
|
95
140
|
{
|
|
96
|
-
ruleId: "
|
|
141
|
+
ruleId: "media.keyAssetTreatment",
|
|
97
142
|
decision: "substituted",
|
|
98
143
|
declaredAt: planned,
|
|
99
144
|
reason: "It did not fit",
|
|
@@ -135,11 +180,41 @@ test("valid development plan", () => {
|
|
|
135
180
|
};
|
|
136
181
|
assert.deepEqual(validateRuleControls(plan, registry, reflex, verification()), []);
|
|
137
182
|
});
|
|
138
|
-
test("invalid
|
|
183
|
+
test("invalid decorative-only expressive plan", () => {
|
|
139
184
|
const plan = basePlan();
|
|
140
|
-
plan.
|
|
185
|
+
plan.pages[0].sections[0].motionTreatment = {
|
|
186
|
+
...plan.pages[0].sections[0].motionTreatment,
|
|
187
|
+
class: "decorative",
|
|
188
|
+
changes: ["The image fades upward and scales into view."],
|
|
189
|
+
mechanism: "CSS opacity, translateY, and scale transitions on scroll reveal.",
|
|
190
|
+
};
|
|
141
191
|
const errors = validateRuleControls(plan, registry, reflex, verification());
|
|
142
|
-
assert.ok(errors.some((error) => error.includes("
|
|
192
|
+
assert.ok(errors.some((error) => error.includes("decorative-only")));
|
|
193
|
+
});
|
|
194
|
+
test("expressive plan requires contextual motion budget", () => {
|
|
195
|
+
const plan = basePlan("expressive");
|
|
196
|
+
delete plan.motionComplexityBudget;
|
|
197
|
+
assert.ok(validateRuleControls(plan, registry, reflex, verification()).some((error) => error.includes("motionComplexityBudget")));
|
|
198
|
+
});
|
|
199
|
+
test("award verification requires timeline states and a refinement pass", () => {
|
|
200
|
+
const plan = basePlan();
|
|
201
|
+
const report = {
|
|
202
|
+
version: 1,
|
|
203
|
+
generatedAt: started,
|
|
204
|
+
evidence: [
|
|
205
|
+
{
|
|
206
|
+
id: "initial-only",
|
|
207
|
+
criterion: "Initial state",
|
|
208
|
+
status: "pass",
|
|
209
|
+
evidence: "Captured only the initial frame.",
|
|
210
|
+
timelineState: "initial",
|
|
211
|
+
proof: { timestamp: started, testedUrl: "http://localhost:3000", consoleErrorCount: 0 },
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
};
|
|
215
|
+
const errors = validateRuleControls(plan, registry, reflex, report);
|
|
216
|
+
assert.ok(errors.some((error) => error.includes("mid-transition")));
|
|
217
|
+
assert.ok(errors.some((error) => error.includes("refinement pass")));
|
|
143
218
|
});
|
|
144
219
|
test("valid reflex-font justification", () => {
|
|
145
220
|
const plan = basePlan("premium");
|
package/package.json
CHANGED
package/skill/dreative/DESIGN.md
CHANGED
|
@@ -156,7 +156,7 @@ its own plan didn't describe. Hard rules for any expressive+ no-media build:
|
|
|
156
156
|
the blueprint's layout-family words couldn't match them, the build
|
|
157
157
|
restyled a card list and called it the spine — that is a depth-honesty
|
|
158
158
|
failure (§11), not a detail.
|
|
159
|
-
2.
|
|
159
|
+
2. **Bespoke drawn/procedural artifacts carry the missing media role.** With no photography, the page
|
|
160
160
|
earns visual richness from things MADE for it: a live canvas (meter,
|
|
161
161
|
field, trace), custom SVG structure (rules, notches, connectors, dials —
|
|
162
162
|
drawn, not icon-font), procedural texture/grain, a data-driven ornament.
|
|
@@ -165,16 +165,14 @@ its own plan didn't describe. Hard rules for any expressive+ no-media build:
|
|
|
165
165
|
3. **Type does structural work.** Multi-scale composition (oversized
|
|
166
166
|
numerals/designators against small mono metadata), real hierarchy per §6
|
|
167
167
|
— not one font-size row layout repeated N times.
|
|
168
|
-
4. **Motion budget spends on the structure**, not on entrance fades: the
|
|
169
|
-
spine's own physicality (things racked, tossed, slid, stamped) is the
|
|
170
|
-
choreography. Motion.md's
|
|
171
|
-
5. **
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
parallax, a lit WebGL surface). A no-media award page with zero rendered
|
|
177
|
-
pixels is a text document with transitions. Vocabulary to draw from
|
|
168
|
+
4. **Motion budget spends on the structure**, not on entrance fades: the
|
|
169
|
+
spine's own physicality (things racked, tossed, slid, stamped) is the
|
|
170
|
+
choreography. Motion.md's treatment and verification rules apply.
|
|
171
|
+
5. **Choose a rendering layer only when it earns its cost.** No imagery can
|
|
172
|
+
justify Canvas/WebGL when a live procedural or data-driven surface is the
|
|
173
|
+
concept, but award ambition does not require one. Spatial typography,
|
|
174
|
+
layered DOM/SVG composition, masks, or other subject-grounded systems may be
|
|
175
|
+
stronger. Vocabulary to draw from when rendering is warranted
|
|
178
176
|
(subject-grounded, not decorative): oscilloscope/waveform traces driven by
|
|
179
177
|
real events · topographic contour lines drifting · dot-matrix/LED field
|
|
180
178
|
that spells live state · plexus line-network connecting related items ·
|
|
@@ -361,10 +359,17 @@ sites win on composition, not decoration; this is where that happens.
|
|
|
361
359
|
- Product register: responsive behavior is structural (collapse sidebar, responsive
|
|
362
360
|
table), not fluid type.
|
|
363
361
|
|
|
364
|
-
## 6. Motion
|
|
365
|
-
|
|
366
|
-
Every animation answers "what does this communicate?" — hierarchy, storytelling,
|
|
367
|
-
feedback, or state change. "It looked cool" = delete it.
|
|
362
|
+
## 6. Motion
|
|
363
|
+
|
|
364
|
+
Every animation answers "what does this communicate?" — hierarchy, storytelling,
|
|
365
|
+
feedback, or state change. "It looked cool" = delete it.
|
|
366
|
+
|
|
367
|
+
Motion has three roles. **Decorative motion** reveals, responds, or adds ambient
|
|
368
|
+
life. **Structural motion** controls hierarchy, pacing, pinning, and composition
|
|
369
|
+
handoffs. **Transformational motion** changes imagery/type/objects into a new
|
|
370
|
+
form or the next scene. Expressive/award pages evaluate key moments for the last
|
|
371
|
+
two roles; opacity, translate, scale, and slight parallax are not automatically
|
|
372
|
+
enough merely because they animate.
|
|
368
373
|
|
|
369
374
|
**The 100/300/500 rule:** 100-150ms instant feedback (press, toggle) · 200-300ms
|
|
370
375
|
state changes (menu, tooltip, hover) · 300-500ms layout changes (accordion, modal)
|
|
@@ -381,9 +386,12 @@ micro-interactions; the uniform fade-and-rise on every scrolled section is the
|
|
|
381
386
|
saturated AI tell. Some brands skip entrance motion entirely — restraint as voice.
|
|
382
387
|
Product = 150-250ms, state-conveying only, zero page-load choreography.
|
|
383
388
|
|
|
384
|
-
**Mechanics:**
|
|
385
|
-
|
|
386
|
-
|
|
389
|
+
**Mechanics:** use transform/opacity for ordinary local state when they fit, but
|
|
390
|
+
do not make them the unexamined expressive default. Select CSS, GSAP timelines,
|
|
391
|
+
ScrollTrigger, SVG, masks, `clip-path`, DOM fragments, Canvas, WebGL/shaders,
|
|
392
|
+
sequences, video, Lottie, or 3D by visual result, cost, responsiveness, and
|
|
393
|
+
available assets. Use the simplest mechanism capable of making the intended
|
|
394
|
+
transformation convincing. Never casually animate width/height/top/left/margins (use FLIP or
|
|
387
395
|
grid-template-rows). Sibling stagger `calc(var(--i) * 50ms)` capped at ~500ms total.
|
|
388
396
|
IntersectionObserver (unobserve after firing), CSS scroll-driven animations, or
|
|
389
397
|
Motion/GSAP scroll tools — `window.addEventListener("scroll")` is a hard ban. Reveals
|
|
@@ -734,11 +742,11 @@ On touch, whatever hover revealed must be visible by default or reachable by
|
|
|
734
742
|
tap — never lost. Custom cursors simply don't exist on touch; don't ship their
|
|
735
743
|
DOM/listeners there.
|
|
736
744
|
|
|
737
|
-
**Pinned/scrubbed sections**: pinning is fragile on mobile (URL-bar resize
|
|
745
|
+
**Pinned/scrubbed sections**: pinning is fragile on mobile (URL-bar resize
|
|
738
746
|
fires viewport changes mid-pin; iOS momentum scroll skips scrub frames).
|
|
739
|
-
Strategies in preference order: (1) keep the pin but shorten it
|
|
740
|
-
|
|
741
|
-
|
|
747
|
+
Strategies in preference order: (1) keep the pin but shorten it and drive it
|
|
748
|
+
with `position: sticky` when reliable; (2) redesign the same idea as a shorter
|
|
749
|
+
mask, layered-parallax, or state handoff sequence rather than merely disabling it;
|
|
742
750
|
(3) `ScrollTrigger.matchMedia()` / a `matchMedia` branch that swaps the whole
|
|
743
751
|
choreography. Never let a desktop pin length ride on a phone.
|
|
744
752
|
|
|
@@ -765,10 +773,10 @@ clamps DOWN gracefully (hero ≥ 2rem, body stays ≥ 16px — iOS zooms inputs
|
|
|
765
773
|
under 16px); sticky navs shrink rather than stack; test the three widths 320 /
|
|
766
774
|
768 / 1280 mentally before responding.
|
|
767
775
|
|
|
768
|
-
**Motion budget on mobile**:
|
|
769
|
-
|
|
770
|
-
off-screen
|
|
771
|
-
|
|
776
|
+
**Motion budget on mobile**: shorten travel and timelines according to content,
|
|
777
|
+
viewport, and measured device cost. Preserve one defining idea when feasible;
|
|
778
|
+
marquees pause off-screen and continuous rAF loops idle when nothing animates.
|
|
779
|
+
Battery is a design constraint, not a reason to erase the concept.
|
|
772
780
|
|
|
773
781
|
## 14. Typography sourcing (escape the first reflex)
|
|
774
782
|
|