dreative 0.1.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.
@@ -0,0 +1,163 @@
1
+ /** Specialist skill detection — keyword signatures over brief/prompt/block text.
2
+ * Names map to skill/dreative/skills/<name>.md. */
3
+ const SKILL_SIGNATURES = {
4
+ "3d": /\b(3d|three\.?js|webgl|r3f|shader|glsl|particles?|point ?cloud|globe|mesh gradient|orbit|spline)\b/i,
5
+ motion: /\b(animat|motion|parallax|scroll[- ]?(driven|trigger|story|choreo)|gsap|framer|lenis|marquee|kinetic|cinematic|stagger|reveal|transition)\b/i,
6
+ interaction: /\b(micro[- ]?interaction|hover (effect|state)s?|cursor|magnetic|tilt|spotlight|glow|ripple|tactile|interactive|draggable)\b/i,
7
+ };
8
+ function detectSkills(texts) {
9
+ const hay = texts.filter(Boolean).join(" \n ");
10
+ return Object.keys(SKILL_SIGNATURES).filter((k) => SKILL_SIGNATURES[k].test(hay));
11
+ }
12
+ /** All text a block subtree carries that can signal a specialist skill. */
13
+ function blockTexts(b) {
14
+ return [
15
+ b.label,
16
+ b.summary,
17
+ ...(b.intents ?? []),
18
+ ...(b.children ?? []).flatMap(blockTexts),
19
+ ].filter((t) => Boolean(t));
20
+ }
21
+ const AESTHETIC_DIALS = {
22
+ minimal: [5, 3, 3],
23
+ editorial: [6, 4, 3],
24
+ premium: [7, 6, 3],
25
+ playful: [9, 8, 3],
26
+ brutalist: [7, 2, 5],
27
+ "dark-tech": [6, 5, 5],
28
+ trust: [3, 2, 5],
29
+ };
30
+ /** Layout families rotated across sections so no page repeats itself. */
31
+ const FAMILIES = [
32
+ "asymmetric-split",
33
+ "bento-grid",
34
+ "full-width-statement",
35
+ "stacked-vertical",
36
+ "offset-two-col",
37
+ "horizontal-scroll-strip",
38
+ "editorial-columns",
39
+ ];
40
+ function resolveDials(brief) {
41
+ const base = AESTHETIC_DIALS[brief?.aesthetic ?? ""] ?? [7, 5, 4];
42
+ return {
43
+ variance: brief?.variance ?? base[0],
44
+ motion: brief?.motion ?? base[1],
45
+ density: brief?.density ?? base[2],
46
+ };
47
+ }
48
+ /** Rough family signature of a block, for repetition detection. */
49
+ function familySignature(b) {
50
+ const kids = b.children ?? [];
51
+ if (b.type === "hero")
52
+ return "hero";
53
+ if (b.type === "card-grid" || (b.direction === "row" && kids.length >= 3))
54
+ return `grid-${kids.length}`;
55
+ if (b.direction === "row" && kids.length === 2) {
56
+ const types = kids.map((k) => k.type).sort().join("+");
57
+ return `split-${types}`;
58
+ }
59
+ return `${b.type}-${b.direction ?? "column"}`;
60
+ }
61
+ function countBlocks(b, pred) {
62
+ return (pred(b) ? 1 : 0) + (b.children ?? []).reduce((n, c) => n + countBlocks(c, pred), 0);
63
+ }
64
+ /** Doctrine lints over the wireframe structure (DESIGN.md §2, §7). */
65
+ export function lintLayout(layout) {
66
+ const lints = [];
67
+ const sections = layout.children ?? [];
68
+ // consecutive repeated layout family (zigzag cap / repetition ban)
69
+ let run = 1;
70
+ for (let i = 1; i < sections.length; i++) {
71
+ if (familySignature(sections[i]) === familySignature(sections[i - 1])) {
72
+ run++;
73
+ if (run === 3)
74
+ lints.push(`3+ consecutive sections share the same layout ("${sections[i].label}") — break the pattern with a different family`);
75
+ }
76
+ else
77
+ run = 1;
78
+ }
79
+ // same family appearing >2 times anywhere on the page
80
+ const sigCount = new Map();
81
+ sections.forEach((s) => sigCount.set(familySignature(s), (sigCount.get(familySignature(s)) ?? 0) + 1));
82
+ for (const [sig, n] of sigCount) {
83
+ if (n > 2 && sig !== "hero")
84
+ lints.push(`layout family "${sig}" used ${n}× — a page needs distinct section compositions`);
85
+ }
86
+ // 3-equal-cards row
87
+ const walk = (b) => {
88
+ const kids = b.children ?? [];
89
+ if (kids.length >= 3 &&
90
+ (b.direction === "row" || b.type === "card-grid") &&
91
+ kids.every((k) => k.type === kids[0].type && (k.sizeHint ?? "md") === (kids[0].sizeHint ?? "md"))) {
92
+ lints.push(`"${b.label}": ${kids.length} identical cells in a row — vary sizes or restructure as an asymmetric bento`);
93
+ }
94
+ if (b.type === "list" && kids.length > 5) {
95
+ lints.push(`"${b.label}": ${kids.length}-item list — group into chunks, cards, or tabs instead of one long list`);
96
+ }
97
+ kids.forEach(walk);
98
+ };
99
+ walk(layout);
100
+ // hero discipline: too much stacked inside a hero
101
+ const heroes = [];
102
+ const findHeroes = (b) => {
103
+ if (b.type === "hero")
104
+ heroes.push(b);
105
+ b.children?.forEach(findHeroes);
106
+ };
107
+ findHeroes(layout);
108
+ for (const h of heroes) {
109
+ const textish = countBlocks(h, (x) => x.type === "text" || x.type === "button");
110
+ if (textish > 4)
111
+ lints.push(`hero "${h.label}" stacks ${textish} text/button elements — max 4 (headline, subtext, CTAs, one eyebrow)`);
112
+ }
113
+ // CTA duplication (very rough: many buttons at top level)
114
+ const buttons = countBlocks(layout, (b) => b.type === "button");
115
+ if (buttons > 5)
116
+ lints.push(`${buttons} buttons on one page — consolidate duplicate CTA intents (one label per intent)`);
117
+ return lints;
118
+ }
119
+ /** Build the compact, executable plan sent to the agent with design-page. */
120
+ export function buildDesignPlan(page, brief) {
121
+ const dials = resolveDials(brief);
122
+ const aesthetic = brief?.aesthetic || "auto (infer per DESIGN.md, then commit)";
123
+ const sections = (page.layout.children ?? []).map((s, i) => {
124
+ let family;
125
+ if (s.type === "nav")
126
+ family = "nav-single-line";
127
+ else if (s.type === "hero")
128
+ family = dials.variance > 4 ? "asymmetric-split-hero" : "centered-hero";
129
+ else if (s.type === "footer")
130
+ family = "footer";
131
+ else
132
+ family = FAMILIES[i % FAMILIES.length];
133
+ const secSkills = detectSkills(blockTexts(s));
134
+ return secSkills.length ? { id: s.id, label: s.label, family, skills: secSkills } : { id: s.id, label: s.label, family };
135
+ });
136
+ // page-level specialist skills: brief/prompt keywords + section hits + motion dial
137
+ const skills = new Set([
138
+ ...detectSkills([brief?.vibe, brief?.notes, page.designPrompt]),
139
+ ...sections.flatMap((s) => s.skills ?? []),
140
+ ]);
141
+ if (dials.motion >= 6)
142
+ skills.add("motion");
143
+ const spacing = dials.density <= 3 ? "py-24/py-32 section gaps" : dials.density <= 6 ? "py-16/py-20" : "py-8/py-12, hairline dividers, mono numerals";
144
+ const motionBudget = dials.motion <= 3
145
+ ? "hover/active states only, no scroll animation"
146
+ : dials.motion <= 6
147
+ ? "entry fade+rise on hero, whileInView reveals on 2-3 key sections, nothing infinite"
148
+ : "scroll choreography allowed on ≤2 sections + hero entry; everything reduced-motion safe";
149
+ const directives = [
150
+ `aesthetic: ${aesthetic}; dials variance=${dials.variance} motion=${dials.motion} density=${dials.density}`,
151
+ `spacing scale: ${spacing}`,
152
+ `motion budget: ${motionBudget}`,
153
+ "one accent color, one neutral family, one radius system, locked across ALL pages of this project",
154
+ "section layout families are assigned below — follow them, do not default to repeated card rows",
155
+ ...(dials.variance > 4 ? ["avoid centered-hero-over-gradient; use split or asymmetric composition"] : []),
156
+ ...(skills.size
157
+ ? [
158
+ `specialist skills required: ${[...skills].join(", ")} — read skills/<name>.md (next to DESIGN.md) before writing code; sections tagged with "skills" get that treatment`,
159
+ ]
160
+ : []),
161
+ ];
162
+ return { dials, aesthetic, sections, directives, lints: lintLayout(page.layout), skills: [...skills] };
163
+ }
@@ -0,0 +1 @@
1
+ export {};