brand-intent 0.6.0 → 0.8.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/package.json +6 -2
- package/parser/brandParser.ts +376 -6
- package/parser/index.ts +13 -0
- package/parser/types.ts +77 -0
- 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 Brand Intent 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 Brand Intent 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brand-intent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "An open file format specification for making brand intent machine-readable.",
|
|
5
5
|
"author": "Christophe Stoll <christophe@stoll.studio>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,7 +33,11 @@
|
|
|
33
33
|
"design-system"
|
|
34
34
|
],
|
|
35
35
|
"scripts": {
|
|
36
|
-
"release": "./scripts/release.sh"
|
|
36
|
+
"release": "./scripts/release.sh",
|
|
37
|
+
"test": "tsx tests/brandParser.test.ts && tsx tests/realBrands.test.ts"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"tsx": "^4.19.0"
|
|
37
41
|
},
|
|
38
42
|
"files": [
|
|
39
43
|
"cli/",
|
package/parser/brandParser.ts
CHANGED
|
@@ -6,6 +6,19 @@
|
|
|
6
6
|
* handles expression-layer primitives only (colors, typography, dividers, assets, badge styles).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import type {
|
|
10
|
+
BrandColorGroup,
|
|
11
|
+
BrandColorEntry,
|
|
12
|
+
ColorMode,
|
|
13
|
+
PaletteStrategy,
|
|
14
|
+
Adherence,
|
|
15
|
+
AdherenceTolerances,
|
|
16
|
+
ColorBounds,
|
|
17
|
+
StateColors,
|
|
18
|
+
GroupKey,
|
|
19
|
+
NeutralRole,
|
|
20
|
+
} from './types';
|
|
21
|
+
|
|
9
22
|
// ── Parsed type ────────────────────────────────────────────────────────────────
|
|
10
23
|
|
|
11
24
|
export interface ParsedBrand {
|
|
@@ -31,10 +44,26 @@ export interface ParsedBrand {
|
|
|
31
44
|
favicon?: string;
|
|
32
45
|
/** Path to OG image asset (relative to /public), used for social sharing previews */
|
|
33
46
|
ogImage?: string;
|
|
34
|
-
/** Core brand color primitives: name → hex (e.g. primary, secondary, accent1, accent2, white)
|
|
47
|
+
/** Core brand color primitives: name → hex (e.g. primary, secondary, accent1, accent2, white).
|
|
48
|
+
* Always present when any colors are declared — grouped-syntax brands get a flat projection
|
|
49
|
+
* generated from all entries, so legacy consumers keep working unchanged. */
|
|
35
50
|
brandColors?: Record<string, string>;
|
|
36
51
|
/** Structured print specs per brand color: name → {pantone?, hks?, cmyk?} */
|
|
37
52
|
brandColorPrint?: Record<string, BrandColorPrintSpec>;
|
|
53
|
+
/** Grouped-syntax representation (v0.8.0+). Present only when the `brand-colors` block uses the grouped form. */
|
|
54
|
+
colorGroups?: BrandColorGroup[];
|
|
55
|
+
/** Per-brand fixed state colors (problem/warning/success/info/inactive). */
|
|
56
|
+
stateColors?: StateColors;
|
|
57
|
+
/** Bounds for guided derivation (luminance + saturation floor). */
|
|
58
|
+
colorBounds?: ColorBounds;
|
|
59
|
+
/** Color resolution mode. Defaults behave as `static` when absent. */
|
|
60
|
+
colorMode?: ColorMode;
|
|
61
|
+
/** Palette strategy default for Path A (guided shuffle). Optional. */
|
|
62
|
+
colorStrategy?: PaletteStrategy;
|
|
63
|
+
/** Adherence level for guided derivation. Optional — engine-side default per level. */
|
|
64
|
+
colorAdherence?: Adherence;
|
|
65
|
+
/** Per-brand tolerance overrides for the declared adherence level. */
|
|
66
|
+
colorAdherenceOverrides?: AdherenceTolerances;
|
|
38
67
|
/** Inline themes: theme name → slot name → resolved hex color (only when tokensSource === 'inline') */
|
|
39
68
|
themes?: Record<string, Record<string, string>>;
|
|
40
69
|
/** Print annotations from inline # comments: slot name → annotation text */
|
|
@@ -131,6 +160,13 @@ export interface ParsedDividerDef {
|
|
|
131
160
|
|
|
132
161
|
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
133
162
|
|
|
163
|
+
const GROUP_KEYWORDS = new Set<string>(['primary', 'secondary', 'accent', 'neutral']);
|
|
164
|
+
const NEUTRAL_ROLES = new Set<NeutralRole>(['light', 'subtle', 'mid', 'muted', 'dark']);
|
|
165
|
+
const STATE_KEYS = new Set<string>(['problem', 'warning', 'success', 'info', 'inactive']);
|
|
166
|
+
const VALID_COLOR_MODES = new Set<ColorMode>(['static', 'dynamic', 'guided']);
|
|
167
|
+
const VALID_STRATEGIES = new Set<PaletteStrategy>(['monochrome', 'duotone', 'tricolor', 'spectrum']);
|
|
168
|
+
const VALID_ADHERENCES = new Set<Adherence>(['strict', 'on-brand', 'flexible']);
|
|
169
|
+
|
|
134
170
|
/**
|
|
135
171
|
* Expand a 3-digit hex to 6-digit. Pass-through for 6 or 8-digit.
|
|
136
172
|
* Returns null for invalid input.
|
|
@@ -244,7 +280,7 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
244
280
|
tokensSource: 'figma',
|
|
245
281
|
};
|
|
246
282
|
|
|
247
|
-
type ActiveBlock = 'none' | 'brand-colors' | 'theme' | 'spacing' | 'divider' | 'typography' | 'font' | 'logo' | 'photo' | 'video' | 'illustration' | 'label' | 'cta';
|
|
283
|
+
type ActiveBlock = 'none' | 'brand-colors' | 'state-colors' | 'color-bounds' | 'color-adherence' | 'theme' | 'spacing' | 'divider' | 'typography' | 'font' | 'logo' | 'photo' | 'video' | 'illustration' | 'label' | 'cta';
|
|
248
284
|
let activeBlock: ActiveBlock = 'none';
|
|
249
285
|
let currentThemeName = '';
|
|
250
286
|
let currentDividerName = '';
|
|
@@ -253,6 +289,18 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
253
289
|
const brandColors: Record<string, string> = {};
|
|
254
290
|
const brandColorPrint: Record<string, BrandColorPrintSpec> = {};
|
|
255
291
|
let currentBrandColorName = '';
|
|
292
|
+
|
|
293
|
+
// Grouped brand-colors state (v0.8.0+).
|
|
294
|
+
// Shape is detected lazily: when the first non-blank indented line inside `brand-colors`
|
|
295
|
+
// is a bare group keyword, the block switches to grouped mode. Otherwise it stays flat.
|
|
296
|
+
type GroupedShape = 'unknown' | 'flat' | 'grouped';
|
|
297
|
+
let brandColorsShape: GroupedShape = 'unknown';
|
|
298
|
+
const colorGroups: BrandColorGroup[] = [];
|
|
299
|
+
let currentGroup: BrandColorGroup | null = null;
|
|
300
|
+
let currentGroupedEntry: BrandColorEntry | null = null;
|
|
301
|
+
const stateColorsRaw: Record<string, string> = {};
|
|
302
|
+
const colorBoundsRaw: Record<string, string> = {};
|
|
303
|
+
const adherenceOverridesRaw: Record<string, string> = {};
|
|
256
304
|
const themes: Record<string, Record<string, string>> = {};
|
|
257
305
|
const themeDescriptions: Record<string, string> = {};
|
|
258
306
|
const spacingRaw: Record<string, string> = {};
|
|
@@ -282,7 +330,123 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
282
330
|
if (isIndented) {
|
|
283
331
|
if (activeBlock === 'brand-colors') {
|
|
284
332
|
const trimmed = raw.trimStart();
|
|
333
|
+
if (trimmed === '') continue;
|
|
285
334
|
const indentLen = raw.length - trimmed.length;
|
|
335
|
+
|
|
336
|
+
// Shape detection: first non-blank indented line decides.
|
|
337
|
+
// Grouped = bare group keyword (`primary`/`secondary`/`accent`/`neutral`) at 2-space indent.
|
|
338
|
+
// Flat = `key: value` at 2-space indent.
|
|
339
|
+
if (brandColorsShape === 'unknown') {
|
|
340
|
+
if (GROUP_KEYWORDS.has(trimmed) && indentLen <= 2) {
|
|
341
|
+
brandColorsShape = 'grouped';
|
|
342
|
+
} else {
|
|
343
|
+
brandColorsShape = 'flat';
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (brandColorsShape === 'grouped') {
|
|
348
|
+
// ── Grouped shape ────────────────────────────────────────────────
|
|
349
|
+
const groupKwMatch = GROUP_KEYWORDS.has(trimmed) && indentLen <= 2;
|
|
350
|
+
if (groupKwMatch) {
|
|
351
|
+
const key = trimmed as GroupKey;
|
|
352
|
+
if (colorGroups.some(g => g.key === key)) {
|
|
353
|
+
throw new Error(`${fileName}:${lineNum}: duplicate color group "${key}"`);
|
|
354
|
+
}
|
|
355
|
+
currentGroup = { key, entries: [] };
|
|
356
|
+
colorGroups.push(currentGroup);
|
|
357
|
+
currentGroupedEntry = null;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (!currentGroup) {
|
|
361
|
+
throw new Error(
|
|
362
|
+
`${fileName}:${lineNum}: grouped brand-colors entries must appear inside a group block (primary/secondary/accent/neutral)`,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
// Group-level properties at 4-space indent OR color-entry header / sub-properties.
|
|
366
|
+
// We distinguish by: entry header = starts with `#` or `$`.
|
|
367
|
+
if (trimmed.startsWith('#') || trimmed.startsWith('$')) {
|
|
368
|
+
// New entry. Header forms:
|
|
369
|
+
// "#HEX Display Name"
|
|
370
|
+
// "$group.role"
|
|
371
|
+
if (trimmed.startsWith('#')) {
|
|
372
|
+
// Split header: "#HEX Name" — hex then whitespace then name
|
|
373
|
+
const headerMatch = trimmed.match(/^(#[0-9a-fA-F]{3,8})(?:\s+(.+))?$/);
|
|
374
|
+
if (!headerMatch) {
|
|
375
|
+
throw new Error(
|
|
376
|
+
`${fileName}:${lineNum}: expected "#HEX Name" entry header, got "${trimmed}"`,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
const hex = normalizeHex(headerMatch[1]);
|
|
380
|
+
if (!hex) {
|
|
381
|
+
throw new Error(
|
|
382
|
+
`${fileName}:${lineNum}: invalid hex "${headerMatch[1]}"`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
const name = (headerMatch[2] ?? '').trim();
|
|
386
|
+
if (!name) {
|
|
387
|
+
throw new Error(
|
|
388
|
+
`${fileName}:${lineNum}: color entry needs a display name after the hex`,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
currentGroupedEntry = { hex, name };
|
|
392
|
+
currentGroup.entries.push(currentGroupedEntry);
|
|
393
|
+
} else {
|
|
394
|
+
// $group.role reference — resolved in finalisation pass.
|
|
395
|
+
const refMatch = trimmed.match(/^\$([a-z]+)\.([a-z-]+)$/i);
|
|
396
|
+
if (!refMatch) {
|
|
397
|
+
throw new Error(
|
|
398
|
+
`${fileName}:${lineNum}: expected "$group.role" reference, got "${trimmed}"`,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
currentGroupedEntry = {
|
|
402
|
+
hex: '',
|
|
403
|
+
name: '',
|
|
404
|
+
referencedFrom: `${refMatch[1]}.${refMatch[2]}`,
|
|
405
|
+
};
|
|
406
|
+
currentGroup.entries.push(currentGroupedEntry);
|
|
407
|
+
}
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
// `key: value` line — either group property (indent ≤ 4) or entry sub-property.
|
|
411
|
+
const colonIdx = trimmed.indexOf(':');
|
|
412
|
+
if (colonIdx === -1) continue;
|
|
413
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
414
|
+
const value = trimmed.slice(colonIdx + 1).trim();
|
|
415
|
+
// Group property lives at indent 4 and only appears before the first entry.
|
|
416
|
+
if (indentLen <= 4 && !currentGroupedEntry) {
|
|
417
|
+
if (key === 'meaning') {
|
|
418
|
+
currentGroup.meaning = value;
|
|
419
|
+
} else if (key === 'distribution') {
|
|
420
|
+
if (currentGroup.key === 'neutral') {
|
|
421
|
+
throw new Error(
|
|
422
|
+
`${fileName}:${lineNum}: neutral group cannot declare distribution — it's the scale, not a selection target`,
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
const pct = parseFloat(value.replace(/%$/, ''));
|
|
426
|
+
if (isNaN(pct)) {
|
|
427
|
+
throw new Error(
|
|
428
|
+
`${fileName}:${lineNum}: distribution "${value}" is not a number`,
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
currentGroup.distribution = pct;
|
|
432
|
+
}
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
// Entry sub-property.
|
|
436
|
+
if (!currentGroupedEntry) continue;
|
|
437
|
+
if (key === 'role') {
|
|
438
|
+
currentGroupedEntry.role = value;
|
|
439
|
+
if (value === 'default') currentGroupedEntry.isDefault = true;
|
|
440
|
+
} else if (key === 'meaning') {
|
|
441
|
+
currentGroupedEntry.meaning = value;
|
|
442
|
+
} else if (key === 'pantone' || key === 'hks' || key === 'cmyk') {
|
|
443
|
+
if (!currentGroupedEntry.print) currentGroupedEntry.print = {};
|
|
444
|
+
currentGroupedEntry.print[key] = value;
|
|
445
|
+
}
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ── Flat shape (legacy) ────────────────────────────────────────────
|
|
286
450
|
const colonIdx = trimmed.indexOf(':');
|
|
287
451
|
if (colonIdx !== -1) {
|
|
288
452
|
const key = trimmed.slice(0, colonIdx).trim();
|
|
@@ -308,6 +472,36 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
308
472
|
}
|
|
309
473
|
continue;
|
|
310
474
|
}
|
|
475
|
+
if (activeBlock === 'state-colors') {
|
|
476
|
+
const trimmed = raw.trimStart();
|
|
477
|
+
const colonIdx = trimmed.indexOf(':');
|
|
478
|
+
if (colonIdx !== -1) {
|
|
479
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
480
|
+
const value = trimmed.slice(colonIdx + 1).trim();
|
|
481
|
+
stateColorsRaw[key] = value;
|
|
482
|
+
}
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
if (activeBlock === 'color-bounds') {
|
|
486
|
+
const trimmed = raw.trimStart();
|
|
487
|
+
const colonIdx = trimmed.indexOf(':');
|
|
488
|
+
if (colonIdx !== -1) {
|
|
489
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
490
|
+
const value = trimmed.slice(colonIdx + 1).trim();
|
|
491
|
+
colorBoundsRaw[key] = value;
|
|
492
|
+
}
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (activeBlock === 'color-adherence') {
|
|
496
|
+
const trimmed = raw.trimStart();
|
|
497
|
+
const colonIdx = trimmed.indexOf(':');
|
|
498
|
+
if (colonIdx !== -1) {
|
|
499
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
500
|
+
const value = trimmed.slice(colonIdx + 1).trim();
|
|
501
|
+
adherenceOverridesRaw[key] = value;
|
|
502
|
+
}
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
311
505
|
if (activeBlock === 'theme' && currentThemeName) {
|
|
312
506
|
// slot: #HEX or $ref # optional comment
|
|
313
507
|
const colonIdx = raw.indexOf(':');
|
|
@@ -406,14 +600,30 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
406
600
|
continue;
|
|
407
601
|
}
|
|
408
602
|
|
|
409
|
-
//
|
|
410
|
-
|
|
411
|
-
|
|
603
|
+
// Blank lines are ignored — they don't close active blocks (grouped brand-colors
|
|
604
|
+
// relies on blank lines between groups for readability).
|
|
412
605
|
if (raw.trim() === '') continue;
|
|
413
606
|
|
|
607
|
+
// Non-indented non-blank line ends any active block
|
|
608
|
+
activeBlock = 'none';
|
|
609
|
+
|
|
414
610
|
// brand-colors — starts the brand color primitives block
|
|
415
611
|
if (raw.trim() === 'brand-colors') {
|
|
416
612
|
activeBlock = 'brand-colors';
|
|
613
|
+
currentGroup = null;
|
|
614
|
+
currentGroupedEntry = null;
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// state-colors — flat block of problem/warning/success/info/inactive
|
|
619
|
+
if (raw.trim() === 'state-colors') {
|
|
620
|
+
activeBlock = 'state-colors';
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// color-bounds — luminance + saturation floor for guided derivation
|
|
625
|
+
if (raw.trim() === 'color-bounds') {
|
|
626
|
+
activeBlock = 'color-bounds';
|
|
417
627
|
continue;
|
|
418
628
|
}
|
|
419
629
|
|
|
@@ -496,18 +706,178 @@ export function parseBrandFile(content: string, fileName: string): ParsedBrand {
|
|
|
496
706
|
case 'cta':
|
|
497
707
|
activeBlock = 'cta';
|
|
498
708
|
break;
|
|
709
|
+
case 'color-mode':
|
|
710
|
+
if (!VALID_COLOR_MODES.has(value as ColorMode)) {
|
|
711
|
+
throw new Error(`${fileName}:${lineNum}: color-mode must be "static", "dynamic", or "guided", got "${value}"`);
|
|
712
|
+
}
|
|
713
|
+
result.colorMode = value as ColorMode;
|
|
714
|
+
break;
|
|
715
|
+
case 'color-strategy':
|
|
716
|
+
if (!VALID_STRATEGIES.has(value as PaletteStrategy)) {
|
|
717
|
+
throw new Error(`${fileName}:${lineNum}: color-strategy must be one of monochrome/duotone/tricolor/spectrum, got "${value}"`);
|
|
718
|
+
}
|
|
719
|
+
result.colorStrategy = value as PaletteStrategy;
|
|
720
|
+
break;
|
|
721
|
+
case 'color-adherence':
|
|
722
|
+
if (!VALID_ADHERENCES.has(value as Adherence)) {
|
|
723
|
+
throw new Error(`${fileName}:${lineNum}: color-adherence must be "strict", "on-brand", or "flexible", got "${value}"`);
|
|
724
|
+
}
|
|
725
|
+
result.colorAdherence = value as Adherence;
|
|
726
|
+
// Optional indented overrides follow.
|
|
727
|
+
activeBlock = 'color-adherence';
|
|
728
|
+
break;
|
|
499
729
|
default:
|
|
500
730
|
// Unknown keys silently ignored for forward compatibility
|
|
501
731
|
break;
|
|
502
732
|
}
|
|
503
733
|
}
|
|
504
734
|
|
|
505
|
-
//
|
|
735
|
+
// ── Finalise grouped brand-colors (v0.8.0+) ────────────────────────────────
|
|
736
|
+
if (colorGroups.length > 0) {
|
|
737
|
+
// Resolve $group.role references in declaration order — a reference may only point
|
|
738
|
+
// to a group earlier in the list than its containing group.
|
|
739
|
+
const resolvedNames = new Set<string>();
|
|
740
|
+
for (let gi = 0; gi < colorGroups.length; gi++) {
|
|
741
|
+
const group = colorGroups[gi];
|
|
742
|
+
for (const entry of group.entries) {
|
|
743
|
+
if (!entry.referencedFrom) {
|
|
744
|
+
// Normal entry — ensure its name doesn't collide (important for flat projection).
|
|
745
|
+
if (resolvedNames.has(entry.name)) {
|
|
746
|
+
throw new Error(`${fileName}: duplicate color name "${entry.name}" in brand-colors`);
|
|
747
|
+
}
|
|
748
|
+
resolvedNames.add(entry.name);
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
// Reference shape: "<group>.<role>"
|
|
752
|
+
const [refGroupKey, refRole] = entry.referencedFrom.split('.');
|
|
753
|
+
const refGroupIdx = colorGroups.findIndex(g => g.key === refGroupKey);
|
|
754
|
+
if (refGroupIdx === -1) {
|
|
755
|
+
throw new Error(`${fileName}: reference "$${entry.referencedFrom}" targets unknown group "${refGroupKey}"`);
|
|
756
|
+
}
|
|
757
|
+
if (refGroupIdx >= gi) {
|
|
758
|
+
throw new Error(`${fileName}: reference "$${entry.referencedFrom}" must point to a group declared earlier (forward/self references forbidden)`);
|
|
759
|
+
}
|
|
760
|
+
const refGroup = colorGroups[refGroupIdx];
|
|
761
|
+
// Find the target entry. `default` matches explicit or first-declared default.
|
|
762
|
+
let target: BrandColorEntry | undefined;
|
|
763
|
+
if (refRole === 'default') {
|
|
764
|
+
target = refGroup.entries.find(e => e.isDefault) ?? refGroup.entries[0];
|
|
765
|
+
} else {
|
|
766
|
+
target = refGroup.entries.find(e => e.role === refRole);
|
|
767
|
+
}
|
|
768
|
+
if (!target) {
|
|
769
|
+
throw new Error(`${fileName}: reference "$${entry.referencedFrom}" — no entry with role "${refRole}" in group "${refGroupKey}"`);
|
|
770
|
+
}
|
|
771
|
+
entry.hex = target.hex;
|
|
772
|
+
entry.name = target.name;
|
|
773
|
+
// Don't add referenced name to resolvedNames — it's already registered under the target.
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Apply default selection rule for identity groups.
|
|
777
|
+
if (group.key !== 'neutral' && group.entries.length > 0) {
|
|
778
|
+
const explicit = group.entries.find(e => e.isDefault);
|
|
779
|
+
if (!explicit) {
|
|
780
|
+
// First-declared wins implicitly.
|
|
781
|
+
group.entries[0].isDefault = true;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Neutral-group role validation.
|
|
786
|
+
if (group.key === 'neutral') {
|
|
787
|
+
for (const entry of group.entries) {
|
|
788
|
+
if (entry.role && !NEUTRAL_ROLES.has(entry.role as NeutralRole)) {
|
|
789
|
+
throw new Error(
|
|
790
|
+
`${fileName}: neutral entry "${entry.name}" role "${entry.role}" is not in the fixed vocabulary (light | subtle | mid | muted | dark)`,
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
result.colorGroups = colorGroups;
|
|
798
|
+
|
|
799
|
+
// Build flat projection so legacy consumers keep working unchanged.
|
|
800
|
+
// Key convention: the entry's display name lowercased, spaces → hyphens.
|
|
801
|
+
// Name collisions already rejected above, so the projection is deterministic.
|
|
802
|
+
for (const group of colorGroups) {
|
|
803
|
+
for (const entry of group.entries) {
|
|
804
|
+
const key = entry.name.toLowerCase().replace(/\s+/g, '-');
|
|
805
|
+
if (!brandColors[key]) {
|
|
806
|
+
brandColors[key] = entry.hex;
|
|
807
|
+
if (entry.print) brandColorPrint[key] = entry.print;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Store brand colors (flat projection)
|
|
506
814
|
if (Object.keys(brandColors).length > 0) {
|
|
507
815
|
result.brandColors = brandColors;
|
|
508
816
|
if (Object.keys(brandColorPrint).length > 0) result.brandColorPrint = brandColorPrint;
|
|
509
817
|
}
|
|
510
818
|
|
|
819
|
+
// ── Finalise state-colors / color-bounds / adherence overrides ────────────
|
|
820
|
+
if (Object.keys(stateColorsRaw).length > 0) {
|
|
821
|
+
const state: StateColors = {};
|
|
822
|
+
for (const [key, value] of Object.entries(stateColorsRaw)) {
|
|
823
|
+
if (!STATE_KEYS.has(key)) continue; // forward-compat: ignore unknown
|
|
824
|
+
const hex = normalizeHex(value);
|
|
825
|
+
if (!hex) {
|
|
826
|
+
throw new Error(`${fileName}: state-colors "${key}" value "${value}" is not a valid hex`);
|
|
827
|
+
}
|
|
828
|
+
(state as Record<string, string>)[key] = hex;
|
|
829
|
+
}
|
|
830
|
+
if (Object.keys(state).length > 0) result.stateColors = state;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
if (Object.keys(colorBoundsRaw).length > 0) {
|
|
834
|
+
const bounds: ColorBounds = {};
|
|
835
|
+
if (colorBoundsRaw.light !== undefined) bounds.light = parseFloat(colorBoundsRaw.light);
|
|
836
|
+
if (colorBoundsRaw.dark !== undefined) bounds.dark = parseFloat(colorBoundsRaw.dark);
|
|
837
|
+
if (colorBoundsRaw['saturation-floor'] !== undefined) {
|
|
838
|
+
bounds.saturationFloor = parseFloat(colorBoundsRaw['saturation-floor']);
|
|
839
|
+
}
|
|
840
|
+
result.colorBounds = bounds;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
if (Object.keys(adherenceOverridesRaw).length > 0) {
|
|
844
|
+
const overrides: AdherenceTolerances = {};
|
|
845
|
+
if (adherenceOverridesRaw['hue-tolerance'] !== undefined) {
|
|
846
|
+
overrides.hueTolerance = parseFloat(adherenceOverridesRaw['hue-tolerance']);
|
|
847
|
+
}
|
|
848
|
+
if (adherenceOverridesRaw['sat-tolerance'] !== undefined) {
|
|
849
|
+
overrides.satTolerance = parseFloat(adherenceOverridesRaw['sat-tolerance']);
|
|
850
|
+
}
|
|
851
|
+
if (Object.keys(overrides).length > 0) result.colorAdherenceOverrides = overrides;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// ── Minimum-structure validation for color-mode: guided ───────────────────
|
|
855
|
+
if (result.colorMode === 'guided') {
|
|
856
|
+
if (colorGroups.length === 0) {
|
|
857
|
+
throw new Error(
|
|
858
|
+
`${fileName}: color-mode: guided requires grouped brand-colors syntax — flat syntax is not sufficient. See the Minimum Structure section in the guided-colors spec.`,
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
const primary = colorGroups.find(g => g.key === 'primary');
|
|
862
|
+
const neutral = colorGroups.find(g => g.key === 'neutral');
|
|
863
|
+
if (!primary) {
|
|
864
|
+
throw new Error(`${fileName}: color-mode: guided requires a "primary" group`);
|
|
865
|
+
}
|
|
866
|
+
if (!neutral) {
|
|
867
|
+
throw new Error(`${fileName}: color-mode: guided requires a "neutral" group`);
|
|
868
|
+
}
|
|
869
|
+
const neutralRoles = new Set(neutral.entries.map(e => e.role).filter(Boolean));
|
|
870
|
+
if (!neutralRoles.has('light')) {
|
|
871
|
+
throw new Error(`${fileName}: color-mode: guided requires neutral group to declare at least role "light"`);
|
|
872
|
+
}
|
|
873
|
+
if (!neutralRoles.has('dark')) {
|
|
874
|
+
throw new Error(`${fileName}: color-mode: guided requires neutral group to declare at least role "dark"`);
|
|
875
|
+
}
|
|
876
|
+
if (primary.entries.length === 0) {
|
|
877
|
+
throw new Error(`${fileName}: color-mode: guided requires primary group to have at least one color`);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
511
881
|
// Finalise inline tokens — resolve $ref values against brandColors, apply optional alpha.
|
|
512
882
|
//
|
|
513
883
|
// Theme slot grammar:
|
package/parser/index.ts
CHANGED
|
@@ -7,6 +7,19 @@ export type { ParsedIdentity, ParsedVoice, ParsedPillars, ParsedAudience, Parsed
|
|
|
7
7
|
|
|
8
8
|
export { parseBrandFile } from './brandParser';
|
|
9
9
|
export type { ParsedBrand, ParsedFontDef, ParsedTypographyDef, ParsedDividerDef, BrandColorPrintSpec, ParsedAssetDef, ParsedBadgeStyle } from './brandParser';
|
|
10
|
+
export type {
|
|
11
|
+
ColorMode,
|
|
12
|
+
PaletteStrategy,
|
|
13
|
+
Adherence,
|
|
14
|
+
AdherenceTolerances,
|
|
15
|
+
ColorBounds,
|
|
16
|
+
StateColors,
|
|
17
|
+
GroupKey,
|
|
18
|
+
IdentityGroupKey,
|
|
19
|
+
NeutralRole,
|
|
20
|
+
BrandColorEntry,
|
|
21
|
+
BrandColorGroup,
|
|
22
|
+
} from './types';
|
|
10
23
|
|
|
11
24
|
export { parseFormatFile } from './formatParser';
|
|
12
25
|
export type { ParsedFormat } from './formatParser';
|
package/parser/types.ts
CHANGED
|
@@ -15,3 +15,80 @@ export interface ZoneInsets {
|
|
|
15
15
|
|
|
16
16
|
/** Text alignment options. */
|
|
17
17
|
export type TextAlign = 'left' | 'center' | 'right' | 'justify';
|
|
18
|
+
|
|
19
|
+
// ── Guided colors (v0.8.0) ────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/** Color resolution mode declared at the brand level. */
|
|
22
|
+
export type ColorMode = 'static' | 'dynamic' | 'guided';
|
|
23
|
+
|
|
24
|
+
/** Palette construction strategy for guided shuffle (Path A only). */
|
|
25
|
+
export type PaletteStrategy = 'monochrome' | 'duotone' | 'tricolor' | 'spectrum';
|
|
26
|
+
|
|
27
|
+
/** Adherence level — how tightly guided derivation follows brand groups. */
|
|
28
|
+
export type Adherence = 'strict' | 'on-brand' | 'flexible';
|
|
29
|
+
|
|
30
|
+
/** Optional per-brand tolerance overrides for the declared adherence level. */
|
|
31
|
+
export interface AdherenceTolerances {
|
|
32
|
+
/** Hue drift tolerance in degrees. */
|
|
33
|
+
hueTolerance?: number;
|
|
34
|
+
/** Saturation variance tolerance in percentage points. */
|
|
35
|
+
satTolerance?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Bounds for all guided derivation — brand scales, state variants, neutral interpolation. */
|
|
39
|
+
export interface ColorBounds {
|
|
40
|
+
/** Max luminance for derived tints (0–1). Never brighter than this. */
|
|
41
|
+
light?: number;
|
|
42
|
+
/** Min luminance for derived shades (0–1). Never darker than this. */
|
|
43
|
+
dark?: number;
|
|
44
|
+
/** Min saturation (0–100) for derived colors — prevents washed-out grays. */
|
|
45
|
+
saturationFloor?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Fixed per-brand state (problem / success / warning / info / inactive) colors. */
|
|
49
|
+
export interface StateColors {
|
|
50
|
+
problem?: string;
|
|
51
|
+
warning?: string;
|
|
52
|
+
success?: string;
|
|
53
|
+
info?: string;
|
|
54
|
+
inactive?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Identity group keys (neutral is structurally distinct). */
|
|
58
|
+
export type IdentityGroupKey = 'primary' | 'secondary' | 'accent';
|
|
59
|
+
/** All four group keys. */
|
|
60
|
+
export type GroupKey = IdentityGroupKey | 'neutral';
|
|
61
|
+
|
|
62
|
+
/** Fixed neutral-role vocabulary — ordered light → dark. */
|
|
63
|
+
export type NeutralRole = 'light' | 'subtle' | 'mid' | 'muted' | 'dark';
|
|
64
|
+
|
|
65
|
+
/** One entry within a brand color group. */
|
|
66
|
+
export interface BrandColorEntry {
|
|
67
|
+
/** Resolved 6-digit hex (uppercase). */
|
|
68
|
+
hex: string;
|
|
69
|
+
/** Display name as authored (`#HEX Name` → "Name"). Reference entries inherit the target's name. */
|
|
70
|
+
name: string;
|
|
71
|
+
/** Authored role (identity groups) or fixed neutral-role vocabulary (neutral group). */
|
|
72
|
+
role?: string;
|
|
73
|
+
/** Marks this entry as the group default. Only meaningful in identity groups. */
|
|
74
|
+
isDefault?: boolean;
|
|
75
|
+
/** Authored meaning string — human-facing only, engine ignores. */
|
|
76
|
+
meaning?: string;
|
|
77
|
+
/** When the entry was declared via a `$group.role` reference, the reference path (informational only). */
|
|
78
|
+
referencedFrom?: string;
|
|
79
|
+
/** Print specs authored on this entry (Pantone, HKS, CMYK). */
|
|
80
|
+
print?: { pantone?: string; hks?: string; cmyk?: string };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** One color group within the `brand-colors` block. */
|
|
84
|
+
export interface BrandColorGroup {
|
|
85
|
+
/** Group key — fixed vocabulary. */
|
|
86
|
+
key: GroupKey;
|
|
87
|
+
/** Authored meaning — human-facing only. */
|
|
88
|
+
meaning?: string;
|
|
89
|
+
/** Selection-probability weight (0–100). Invalid on neutral. */
|
|
90
|
+
distribution?: number;
|
|
91
|
+
/** Ordered entries as declared. */
|
|
92
|
+
entries: BrandColorEntry[];
|
|
93
|
+
}
|
|
94
|
+
|
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 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.
|