brand-intent 0.5.1 → 0.7.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 +10 -1
- package/cli/index.mjs +17 -11
- package/examples/krume/krume.brand +0 -2
- package/package.json +1 -1
- package/parser/brandParser.ts +129 -22
- package/parser/purposeParser.ts +1 -1
- package/skills/brief.md +197 -0
package/README.md
CHANGED
|
@@ -34,12 +34,21 @@ ext install studiostoll.brand-intent-lang
|
|
|
34
34
|
|
|
35
35
|
### 2. Run init
|
|
36
36
|
|
|
37
|
-
Installs
|
|
37
|
+
Installs two skills into your AI agent: `/brand-intent` and `/brief`. Together they cover the full loop. Onboard your brand, explore the Krume bakery reference, generate and validate on-brand content, and export a portable brief.
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
40
|
npx brand-intent init
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
### 3. Use your brand
|
|
44
|
+
|
|
45
|
+
Two skills are now installed in your AI agent.
|
|
46
|
+
|
|
47
|
+
- **`/brand-intent`** authors, validates, and generates on-brand content from your DSL files.
|
|
48
|
+
- **`/brief`** exports a portable `{brand-id}-brief.md`. Use it to hand the brand to a tool that doesn't read Brand Intent directly, like a Claude design project, another LLM, or a collaborator.
|
|
49
|
+
|
|
50
|
+
Edit your DSL files whenever strategy or expression evolves. Regenerate briefs after any change.
|
|
51
|
+
|
|
43
52
|
## Documentation
|
|
44
53
|
|
|
45
54
|
- [Specification](SPEC.md): formal grammar for all five file types
|
package/cli/index.mjs
CHANGED
|
@@ -9,7 +9,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
9
9
|
const __dirname = dirname(__filename);
|
|
10
10
|
const PKG_ROOT = resolve(__dirname, '..');
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const SKILL_FILES = ['brand-intent.md', 'brief.md'];
|
|
13
13
|
|
|
14
14
|
// ── Harness detection ──
|
|
15
15
|
|
|
@@ -69,12 +69,15 @@ function scaffoldExample(targetDir) {
|
|
|
69
69
|
// ── Install skill ──
|
|
70
70
|
|
|
71
71
|
async function installSkills(cwd) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
const skills = SKILL_FILES.map(name => {
|
|
73
|
+
const path = join(PKG_ROOT, 'skills', name);
|
|
74
|
+
if (!existsSync(path)) {
|
|
75
|
+
console.error('Error: skill file not found at', path);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
return { name, content: readFileSync(path, 'utf-8') };
|
|
79
|
+
});
|
|
76
80
|
|
|
77
|
-
const skill = readFileSync(SKILL_FILE, 'utf-8');
|
|
78
81
|
let harnesses = detectHarnesses(cwd);
|
|
79
82
|
|
|
80
83
|
if (harnesses.length === 0) {
|
|
@@ -98,9 +101,11 @@ async function installSkills(cwd) {
|
|
|
98
101
|
for (const { label, dir } of harnesses) {
|
|
99
102
|
const target = join(cwd, dir);
|
|
100
103
|
mkdirSync(target, { recursive: true });
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
+
for (const { name, content } of skills) {
|
|
105
|
+
const dest = join(target, name);
|
|
106
|
+
writeFileSync(dest, content, 'utf-8');
|
|
107
|
+
console.log(` ✓ ${label} — ${dir}/${name}`);
|
|
108
|
+
}
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
return harnesses;
|
|
@@ -121,7 +126,7 @@ if (command && command !== 'init' && command !== 'install-skills' && command !==
|
|
|
121
126
|
|
|
122
127
|
if (!command || command === '--help' || command === '-h') {
|
|
123
128
|
console.log('Usage:');
|
|
124
|
-
console.log(' brand-intent init Install the Brand Intent
|
|
129
|
+
console.log(' brand-intent init Install the Brand Intent and Brief skills for your AI agent');
|
|
125
130
|
console.log(' brand-intent install-skills Same as init (alias)');
|
|
126
131
|
console.log('');
|
|
127
132
|
console.log('Example:');
|
|
@@ -138,7 +143,7 @@ if (targetDir !== cwd) {
|
|
|
138
143
|
}
|
|
139
144
|
console.log('\n This will:');
|
|
140
145
|
console.log(' \u2014 Copy the Krume bakery reference into examples/krume/');
|
|
141
|
-
console.log(' \u2014 Install the Brand Intent
|
|
146
|
+
console.log(' \u2014 Install the Brand Intent and Brief skills for your AI agent\n');
|
|
142
147
|
const proceed = await ask(' Proceed? (Y/n) ');
|
|
143
148
|
if (proceed.trim().toLowerCase() === 'n') {
|
|
144
149
|
closeRl();
|
|
@@ -158,4 +163,5 @@ console.log(' /brand-intent onboard Create your brand through a guided in
|
|
|
158
163
|
console.log(' /brand-intent example Scaffold the Krume reference implementation');
|
|
159
164
|
console.log(' /brand-intent validate Validate content against your brand rules');
|
|
160
165
|
console.log(' /brand-intent Discover brand files and available purposes');
|
|
166
|
+
console.log(' /brief Generate a portable brand brief');
|
|
161
167
|
console.log('');
|
package/package.json
CHANGED
package/parser/brandParser.ts
CHANGED
|
@@ -123,19 +123,117 @@ export interface ParsedDividerDef {
|
|
|
123
123
|
thickness: number;
|
|
124
124
|
/** Line width — percentage string (e.g. '15%') or '100%' */
|
|
125
125
|
width: string;
|
|
126
|
-
/**
|
|
127
|
-
* Resolved color value. Supports:
|
|
128
|
-
* - Brand color names: 'accent1', 'accent2', 'primary', 'secondary', etc. (resolved to hex at parse time)
|
|
129
|
-
* - Raw hex: '#E32D39'
|
|
130
|
-
* - Theme slot names: 'logo' | 'cta' | 'sticker' | 'background' | 'text-primary' | … (resolved at render time via resolveDividerColor)
|
|
131
|
-
*/
|
|
132
|
-
color: string;
|
|
133
126
|
/** Spacing token name applied above and below the divider (xs | s | m | l | xl) */
|
|
134
127
|
spacing: string;
|
|
135
128
|
/** Horizontal alignment of a partial-width divider (default: 'left') */
|
|
136
129
|
align: 'left' | 'center' | 'right';
|
|
137
130
|
}
|
|
138
131
|
|
|
132
|
+
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Expand a 3-digit hex to 6-digit. Pass-through for 6 or 8-digit.
|
|
136
|
+
* Returns null for invalid input.
|
|
137
|
+
*/
|
|
138
|
+
function normalizeHex(hex: string): string | null {
|
|
139
|
+
const m = hex.match(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/);
|
|
140
|
+
if (!m) return null;
|
|
141
|
+
const body = m[1];
|
|
142
|
+
if (body.length === 3) {
|
|
143
|
+
return '#' + body.split('').map(c => c + c).join('').toUpperCase();
|
|
144
|
+
}
|
|
145
|
+
return '#' + body.toUpperCase();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Parse a 6-digit hex into [r, g, b]. Input must be normalised to 6 digits. */
|
|
149
|
+
function hexToRgb(hex6: string): [number, number, number] {
|
|
150
|
+
const body = hex6.slice(1);
|
|
151
|
+
return [
|
|
152
|
+
parseInt(body.slice(0, 2), 16),
|
|
153
|
+
parseInt(body.slice(2, 4), 16),
|
|
154
|
+
parseInt(body.slice(4, 6), 16),
|
|
155
|
+
];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Resolve a theme-slot value string to a final CSS color.
|
|
160
|
+
*
|
|
161
|
+
* Supports: `$ref`, `$ref NN%`, `#HEX`, `#HEX NN%`, raw rgba(...) pass-through.
|
|
162
|
+
* Throws on malformed alpha or unresolved `$ref`.
|
|
163
|
+
*/
|
|
164
|
+
function resolveThemeSlotValue(
|
|
165
|
+
raw: string,
|
|
166
|
+
brandColors: Record<string, string>,
|
|
167
|
+
fileName: string,
|
|
168
|
+
slot: string,
|
|
169
|
+
): string {
|
|
170
|
+
const value = raw.trim();
|
|
171
|
+
|
|
172
|
+
// Legacy rgba(...) literal — pass through unchanged.
|
|
173
|
+
if (/^rgba?\s*\(/i.test(value)) return value;
|
|
174
|
+
|
|
175
|
+
// Detect an alpha suffix: "<base> NN%" (one or more spaces, numeric, trailing %).
|
|
176
|
+
// If the line contains a % but the shape doesn't match, it's malformed.
|
|
177
|
+
let base = value;
|
|
178
|
+
let alpha: number | null = null;
|
|
179
|
+
const alphaMatch = value.match(/^(\S+)\s+([0-9]+(?:\.[0-9]+)?)%$/);
|
|
180
|
+
if (alphaMatch) {
|
|
181
|
+
base = alphaMatch[1];
|
|
182
|
+
alpha = parseFloat(alphaMatch[2]);
|
|
183
|
+
if (alpha < 0 || alpha > 100) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`${fileName}: theme slot "${slot}" alpha must be between 0 and 100, got ${alpha}`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
} else if (value.includes('%')) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`${fileName}: theme slot "${slot}" alpha syntax is "<value> NN%" with a single space, got "${value}"`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Resolve base to a hex string.
|
|
195
|
+
let hex: string | null = null;
|
|
196
|
+
if (base.startsWith('$')) {
|
|
197
|
+
const refName = base.slice(1);
|
|
198
|
+
const resolved = brandColors[refName];
|
|
199
|
+
if (!resolved) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`${fileName}: theme slot "${slot}" references unknown brand color "$${refName}"`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
hex = normalizeHex(resolved);
|
|
205
|
+
if (!hex) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`${fileName}: theme slot "${slot}" base "$${refName}" resolves to "${resolved}", which is not a valid hex color`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
hex = normalizeHex(base);
|
|
212
|
+
if (!hex) {
|
|
213
|
+
// Not a hex, not a $ref — but it might be a bare keyword like "currentColor" or
|
|
214
|
+
// a semantic slot name used for dividers historically. Pass through only when
|
|
215
|
+
// no alpha was requested; with alpha, the base must be concrete.
|
|
216
|
+
if (alpha !== null) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`${fileName}: theme slot "${slot}" base "${base}" must be "$ref" or "#HEX" when using alpha`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
return value;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// No alpha → solid hex.
|
|
226
|
+
if (alpha === null) return hex;
|
|
227
|
+
|
|
228
|
+
// 100% alpha → emit solid hex (keep output clean).
|
|
229
|
+
if (alpha === 100) return hex;
|
|
230
|
+
|
|
231
|
+
const [r, g, b] = hexToRgb(hex);
|
|
232
|
+
// Drop trailing zeros for cleanliness: 0.7 not 0.70, 0.5 not 0.50.
|
|
233
|
+
const a = Number((alpha / 100).toFixed(4)).toString();
|
|
234
|
+
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
|
235
|
+
}
|
|
236
|
+
|
|
139
237
|
// ── Parser ─────────────────────────────────────────────────────────────────────
|
|
140
238
|
|
|
141
239
|
export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
@@ -191,6 +289,12 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
191
289
|
const value = trimmed.slice(colonIdx + 1).trim();
|
|
192
290
|
if (indentLen <= 2) {
|
|
193
291
|
// Top-level color line: " primary: #0324B1"
|
|
292
|
+
// Brand colors are solid primitives only — alpha belongs in theme slots.
|
|
293
|
+
if (/%/.test(value) || /rgba\s*\(/i.test(value)) {
|
|
294
|
+
throw new Error(
|
|
295
|
+
`${fileName}:${lineNum}: brand color "${key}" must be a solid hex — alpha syntax (e.g. "$ref 75%" or rgba()) is only allowed in theme slots`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
194
298
|
brandColors[key] = value;
|
|
195
299
|
currentBrandColorName = key;
|
|
196
300
|
} else if (currentBrandColorName) {
|
|
@@ -236,6 +340,11 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
236
340
|
if (colonIdx !== -1) {
|
|
237
341
|
const key = raw.slice(0, colonIdx).trim();
|
|
238
342
|
const value = raw.slice(colonIdx + 1).trim();
|
|
343
|
+
if (key === 'color') {
|
|
344
|
+
throw new Error(
|
|
345
|
+
`${fileName}:${lineNum}: divider "${currentDividerName}": color is no longer configurable. Remove the "color:" line — divider color is sourced from the theme's "divider" slot.`,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
239
348
|
if (!dividersRaw[currentDividerName]) dividersRaw[currentDividerName] = {};
|
|
240
349
|
dividersRaw[currentDividerName][key] = value;
|
|
241
350
|
}
|
|
@@ -399,19 +508,21 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
399
508
|
if (Object.keys(brandColorPrint).length > 0) result.brandColorPrint = brandColorPrint;
|
|
400
509
|
}
|
|
401
510
|
|
|
402
|
-
// Finalise inline tokens — resolve $ref values against brandColors
|
|
511
|
+
// Finalise inline tokens — resolve $ref values against brandColors, apply optional alpha.
|
|
512
|
+
//
|
|
513
|
+
// Theme slot grammar:
|
|
514
|
+
// slot: $ref → resolve ref to hex
|
|
515
|
+
// slot: $ref 75% → resolve ref, then apply alpha (rgba)
|
|
516
|
+
// slot: #HEX → pass through
|
|
517
|
+
// slot: #HEX 30% → apply alpha (rgba)
|
|
518
|
+
// slot: rgba(...) → pass through (legacy, discouraged)
|
|
519
|
+
//
|
|
520
|
+
// Alpha is expressed as `<number>%`, 0–100, separated from the base value by exactly one space.
|
|
521
|
+
// 100% emits solid hex (not rgba) to keep output clean.
|
|
403
522
|
if (Object.keys(themes).length > 0) {
|
|
404
523
|
for (const theme of Object.values(themes)) {
|
|
405
524
|
for (const [slot, value] of Object.entries(theme)) {
|
|
406
|
-
|
|
407
|
-
const refName = value.slice(1);
|
|
408
|
-
const resolved = brandColors[refName];
|
|
409
|
-
if (resolved) {
|
|
410
|
-
theme[slot] = resolved;
|
|
411
|
-
} else {
|
|
412
|
-
console.warn(`${fileName}: unresolved brand color reference "$${refName}" in theme slot "${slot}"`);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
525
|
+
theme[slot] = resolveThemeSlotValue(value, brandColors, fileName, slot);
|
|
415
526
|
}
|
|
416
527
|
}
|
|
417
528
|
result.themes = themes;
|
|
@@ -428,12 +539,9 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
428
539
|
};
|
|
429
540
|
}
|
|
430
541
|
|
|
431
|
-
// Finalise dividers
|
|
542
|
+
// Finalise dividers — color is always sourced from the active theme's "divider" slot.
|
|
432
543
|
if (Object.keys(dividersRaw).length > 0) {
|
|
433
544
|
const dividers: Record<string, ParsedDividerDef> = {};
|
|
434
|
-
// Resolve a color value: brand color name (accent1, accent2, …) → hex,
|
|
435
|
-
// semantic slot names → kept as-is for resolveDividerColor(), raw hex → pass through.
|
|
436
|
-
const resolveColor = (c: string): string => brandColors[c] ?? c;
|
|
437
545
|
for (const [name, raw] of Object.entries(dividersRaw)) {
|
|
438
546
|
const alignRaw = raw['align'] ?? 'left';
|
|
439
547
|
const align: ParsedDividerDef['align'] =
|
|
@@ -441,7 +549,6 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
441
549
|
dividers[name] = {
|
|
442
550
|
thickness: parseFloat(raw['thickness'] ?? '0.2'),
|
|
443
551
|
width: raw['width'] ?? '100%',
|
|
444
|
-
color: resolveColor(raw['color'] ?? 'text-tertiary'),
|
|
445
552
|
spacing: raw['spacing'] ?? 'xs',
|
|
446
553
|
align,
|
|
447
554
|
};
|
package/parser/purposeParser.ts
CHANGED
|
@@ -67,7 +67,7 @@ export interface ParsedPurposeSlot {
|
|
|
67
67
|
itemGap?: number;
|
|
68
68
|
/** List layout direction: 'vertical' (default) or 'horizontal'. */
|
|
69
69
|
listDirection?: 'vertical' | 'horizontal';
|
|
70
|
-
/** When
|
|
70
|
+
/** When false, this slot is locked — users cannot edit it in any mode. Default: true (editable). */
|
|
71
71
|
editable?: boolean;
|
|
72
72
|
typography: {
|
|
73
73
|
fontFamily: string;
|
package/skills/brief.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Generate a portable markdown brand brief from Brand Intent DSL files
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Brief
|
|
6
|
+
|
|
7
|
+
**Scope:** `$ARGUMENTS`
|
|
8
|
+
|
|
9
|
+
> If `$ARGUMENTS` is empty: auto-detect the active brand from the current working directory (see File Discovery in the Brand Intent skill) and generate its brief.
|
|
10
|
+
> If `$ARGUMENTS` names a brand id (e.g. "krume", "studio-stoll"): locate that brand folder under the current working directory and generate its brief.
|
|
11
|
+
> If no brand is found: tell the user to run `/brand-intent onboard` (or `npx brand-intent init` to scaffold the Krume example) and stop.
|
|
12
|
+
|
|
13
|
+
You are generating a **portable brand brief** — a single markdown file, `{brand-id}-brief.md`, written at the brand root. The brief is format-agnostic: it can be dropped into any tool that doesn't read Brand Intent DSL files directly — a Claude design project, another LLM, a human collaborator.
|
|
14
|
+
|
|
15
|
+
The brief is **derived**, not authored. Every value comes verbatim from the DSL files. You never rephrase, interpret, or invent.
|
|
16
|
+
|
|
17
|
+
## When to Regenerate
|
|
18
|
+
|
|
19
|
+
The brief is a snapshot. Regenerate it after any change to `.identity`, `.brand`, `.purpose`, `.format`, or `.composition` files. The header always stamps the generation timestamp and the Brand Intent DSL version so stale briefs are detectable.
|
|
20
|
+
|
|
21
|
+
## Procedure
|
|
22
|
+
|
|
23
|
+
1. **Resolve the brand root.** Find the folder containing both `{brand-id}.identity` and `{brand-id}.brand`. If either file is missing, stop and tell the user to run `/brand-intent onboard`.
|
|
24
|
+
|
|
25
|
+
2. **Read the required files.**
|
|
26
|
+
- `{brand-id}.identity` — strategic layer
|
|
27
|
+
- `{brand-id}.brand` — derived expression layer
|
|
28
|
+
|
|
29
|
+
3. **Read optional files if present.** Each becomes a labeled optional section in the brief. If absent, omit the section entirely — do not emit empty headers.
|
|
30
|
+
- `{brand-id}.purpose` at brand root (rare)
|
|
31
|
+
- any files under `purposes/` (multiple allowed)
|
|
32
|
+
- any files under `formats/`
|
|
33
|
+
- any files under `compositions/`
|
|
34
|
+
|
|
35
|
+
4. **Read the Brand Intent DSL version.** Try resolving the installed `brand-intent` package's `package.json` version. If unreachable (the skill runs in a harness, not always inside the brand-intent repo), use `"unknown"`.
|
|
36
|
+
|
|
37
|
+
5. **Compose the brief** per the template below. Extract values verbatim — never rephrase or interpret. If a field is absent in the DSL, omit its row; never invent.
|
|
38
|
+
|
|
39
|
+
6. **Auto-derive the Guardrails section** from `.identity`:
|
|
40
|
+
- every item in `voice.never`
|
|
41
|
+
- every item in `voice.always`
|
|
42
|
+
- `pillars.avoid` list
|
|
43
|
+
- every `anti-audience` block (label + description)
|
|
44
|
+
|
|
45
|
+
The Guardrails *section in the brief* is derived. Never add a `guardrails:` block to the DSL — guardrails live inside `.identity` already.
|
|
46
|
+
|
|
47
|
+
7. **Write the file.** Output path: `{brand-root}/{brand-id}-brief.md`. If the file exists, confirm overwrite in one line before proceeding.
|
|
48
|
+
|
|
49
|
+
8. **Report.** Print one line: `Wrote {path}. Regenerate after any DSL change.`
|
|
50
|
+
|
|
51
|
+
## Output Template
|
|
52
|
+
|
|
53
|
+
Write exactly this structure. Each section includes a one-line **role description** so the brief self-documents when pasted into any context.
|
|
54
|
+
|
|
55
|
+
```markdown
|
|
56
|
+
# {Brand Name} — Brand Brief
|
|
57
|
+
|
|
58
|
+
> Portable brief derived from Brand Intent DSL files.
|
|
59
|
+
> Brand Intent version: {version} · Generated: {ISO timestamp}
|
|
60
|
+
> Source of truth lives in the DSL files. Regenerate after any DSL change.
|
|
61
|
+
|
|
62
|
+
## Essence
|
|
63
|
+
_Role: the irreducible truth of the brand._
|
|
64
|
+
|
|
65
|
+
{identity.essence}
|
|
66
|
+
|
|
67
|
+
## Promise
|
|
68
|
+
_Role: what the brand commits to deliver._
|
|
69
|
+
|
|
70
|
+
{identity.promise}
|
|
71
|
+
|
|
72
|
+
## Positioning / Mission / Vision / Tagline / Archetype
|
|
73
|
+
_Role: strategic frame. Only rows that exist in the identity are emitted._
|
|
74
|
+
|
|
75
|
+
- **Positioning:** {...}
|
|
76
|
+
- **Mission:** {...}
|
|
77
|
+
- (etc. — skip any row whose field is absent)
|
|
78
|
+
|
|
79
|
+
## Narratives
|
|
80
|
+
_Role: the brand's stories. The primary narrative is the default story._
|
|
81
|
+
|
|
82
|
+
- **{name}** (primary) — {text}
|
|
83
|
+
- **{name}** — {text}
|
|
84
|
+
|
|
85
|
+
## Voice
|
|
86
|
+
_Role: how this brand sounds._
|
|
87
|
+
|
|
88
|
+
- **Register:** {voice.register}
|
|
89
|
+
- **Persona:** {voice.persona}
|
|
90
|
+
- **Rhythm:** {voice.rhythm}
|
|
91
|
+
|
|
92
|
+
## Values
|
|
93
|
+
_Role: behavior statements, not keywords._
|
|
94
|
+
|
|
95
|
+
- **{name}** — {behavior}
|
|
96
|
+
|
|
97
|
+
## Content Pillars
|
|
98
|
+
_Role: what the brand talks about, and what it avoids._
|
|
99
|
+
|
|
100
|
+
- **Primary:** {pillars.primary}
|
|
101
|
+
- **Secondary:** {pillars.secondary}
|
|
102
|
+
- **Avoid:** {pillars.avoid}
|
|
103
|
+
|
|
104
|
+
## Audiences
|
|
105
|
+
_Role: who the brand speaks to._
|
|
106
|
+
|
|
107
|
+
For each audience segment in `.identity`:
|
|
108
|
+
|
|
109
|
+
- **{label}**
|
|
110
|
+
- Profile: {...}
|
|
111
|
+
- Motivation: {...}
|
|
112
|
+
- Language guidance: {...}
|
|
113
|
+
|
|
114
|
+
## Expression (from .brand)
|
|
115
|
+
_Role: derived expression conditions — the "how it looks and sounds" layer, not expression itself._
|
|
116
|
+
|
|
117
|
+
- **Language / locale:** {brand.language} / {brand.locale}
|
|
118
|
+
- **Color primitives:** list each `brand-colors` entry as `name → hex`
|
|
119
|
+
- **Themes:** list each theme with its slot-to-color mappings
|
|
120
|
+
- **Typography styles:** for each named style — font, weight, size (always append unit `cqh` — container-query-height, responsive to the canvas height), lineHeight, letterSpacing, flags like `uppercase`, autofit-min/max if present
|
|
121
|
+
- **Fonts:** for each font block — name, source, variants
|
|
122
|
+
- **Spacing scale:** unit + named steps (e.g. `cqmin` — container-query-min, responsive to the shorter canvas side; xs/s/m/l/xl)
|
|
123
|
+
- **Dividers:** if present, list each
|
|
124
|
+
- **Voice constraints:** `sentence-max`, `headline-pattern`, `number-format`, `register` (if absent in `.brand`, note "not declared" — do not omit the row)
|
|
125
|
+
|
|
126
|
+
## Guardrails (auto-derived)
|
|
127
|
+
_Role: hard rules any generator, agent, or reviewer must enforce._
|
|
128
|
+
|
|
129
|
+
**Never:**
|
|
130
|
+
- {each voice.never item}
|
|
131
|
+
|
|
132
|
+
**Always:**
|
|
133
|
+
- {each voice.always item}
|
|
134
|
+
|
|
135
|
+
**Avoid topics:**
|
|
136
|
+
- {each pillars.avoid item}
|
|
137
|
+
|
|
138
|
+
**Anti-audiences:**
|
|
139
|
+
- **{label}** — {description}
|
|
140
|
+
|
|
141
|
+
## Purposes (optional)
|
|
142
|
+
_Role: content types — what each piece of content is trying to do. Only emitted if any `.purpose` files are present._
|
|
143
|
+
|
|
144
|
+
For each purpose, emit **glossed** tags — not verbatim DSL values:
|
|
145
|
+
|
|
146
|
+
- **{id}** — {name}
|
|
147
|
+
- Compositions: {composition ids}
|
|
148
|
+
- **Density:** gloss the value — `light` = "minimal slots filled, headline-forward"; `medium` = "balanced slot fill"; `full` = "all slots populated". Emit as `{value} — "{gloss}"`.
|
|
149
|
+
- **Palette:** gloss — `dynamic` = "inherits theme colors at render time"; `fixed` = "uses a specific theme"; other values pass through verbatim.
|
|
150
|
+
- **Uses photo:** translate `camera:` field — `true` → "yes"; `false` → "no". Do not emit "camera: false".
|
|
151
|
+
- **Logo:** gloss — `text` = "wordmark (text logo)"; `mark` = "graphic mark"; other values pass through verbatim.
|
|
152
|
+
- Scope: {scope audience + pillars}
|
|
153
|
+
- Context: {context prose per slot}
|
|
154
|
+
- Slots: for each slot — id, typography, maxLength, samples. Omit `color:` references (theme-internal detail). Override hints (`hyphenate`, `textAlign`) pass through as-is.
|
|
155
|
+
|
|
156
|
+
## Formats (optional)
|
|
157
|
+
_Role: where content appears. Only emitted if any `.format` files are present._
|
|
158
|
+
|
|
159
|
+
For each format, label axes and units explicitly:
|
|
160
|
+
|
|
161
|
+
- **{id}** ({label} {sublabel}) — category: {category}
|
|
162
|
+
- **Size:** {width} × {height} px
|
|
163
|
+
- **Grid:** {rows} rows × {cols} cols — always state which number is which. The DSL writes `grid: ROWS COLS`.
|
|
164
|
+
- **Danger zone:** {top} {right} {bottom} {left} px (platform UI overlay insets)
|
|
165
|
+
- **Crop:** {top} {right} {bottom} {left} px, or "none"
|
|
166
|
+
- **Comfort zone:** {value}% inset from the usable area
|
|
167
|
+
|
|
168
|
+
## Compositions (optional)
|
|
169
|
+
_Role: spatial arrangement of slots. Only emitted if any `.composition` files are present._
|
|
170
|
+
|
|
171
|
+
For each composition, gloss the layout mode and decode positioning tokens:
|
|
172
|
+
|
|
173
|
+
- **{id}** ({name})
|
|
174
|
+
- **Mode:** gloss — `flow` = "sequential vertical stack of slots with tunable spacers between them"; `grid` = "2D cell placement on the format grid". Emit as `{mode} — "{gloss}"`.
|
|
175
|
+
- **Image:** if present, state whether it's full-bleed or panel-scoped (`full` → "full-bleed background", other values pass through).
|
|
176
|
+
- **Sticky bar:** translate `sticky: top m` as "pinned to top edge, size/padding `m` (from spacing scale)"; list each item (e.g. `label left`, `logo right`) with position noted.
|
|
177
|
+
- **Flow:** emit the raw flow string for precision, then a one-line prose gloss describing what the layout visually produces. Decode tokens:
|
|
178
|
+
- `spacer-xs/s/m/l/xl` — spacer using the named step from the spacing scale
|
|
179
|
+
- `spacer-auto` — flexible spacer that absorbs remaining space
|
|
180
|
+
- `{slot} left/center/right` — slot with its text alignment
|
|
181
|
+
- `panel {anchor} {width} {height}` — a constrained panel anchored to an edge or corner (e.g. `panel bottom-left 2/3 1/1` = "panel anchored to bottom-left, 2/3 canvas width × full canvas height")
|
|
182
|
+
- `split {edge} {fraction}` — the canvas is split at the given edge and fraction (e.g. `split bottom 1/2` = "canvas split horizontally, content occupies bottom half")
|
|
183
|
+
- `divider-{name}` — references a divider style from `.brand`
|
|
184
|
+
- `columns:N gap:{size}` — the slot renders as N columns with the named gap
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
Generated by `/brief` from brand-intent v{version}. To change anything in this brief, edit the DSL files and regenerate.
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Rules
|
|
191
|
+
|
|
192
|
+
- **No rephrasing.** Values are extracted verbatim from the DSL files.
|
|
193
|
+
- **No interpretation.** If a field is ambiguous, emit it as-is; don't explain it.
|
|
194
|
+
- **Omit absent optional sections entirely.** No empty headers, no "none" placeholders.
|
|
195
|
+
- **Never add `guardrails:` to the DSL.** The Guardrails section in the brief is always derived from existing `.identity` fields.
|
|
196
|
+
- **Never invent.** If `voice.always` is empty, the Always list is empty. Don't fill gaps.
|
|
197
|
+
- **Claude design is an example, not a dependency.** This brief works anywhere. Don't phrase it as Claude-specific.
|