@forgespace/branding-mcp 0.4.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/.env.example +3 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +22 -0
- package/.github/workflows/ci.yml +73 -0
- package/.github/workflows/release-automation.yml +56 -0
- package/.github/workflows/security-scan.yml +37 -0
- package/.gitleaks.toml +14 -0
- package/.prettierrc.json +10 -0
- package/CHANGELOG.md +66 -0
- package/CONTRIBUTING.md +203 -0
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/data/README.md +13 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/branding-core/ai/brand-interpreter.d.ts +5 -0
- package/dist/lib/branding-core/ai/brand-interpreter.d.ts.map +1 -0
- package/dist/lib/branding-core/ai/brand-interpreter.js +16 -0
- package/dist/lib/branding-core/ai/brand-interpreter.js.map +1 -0
- package/dist/lib/branding-core/ai/claude-interpreter.d.ts +5 -0
- package/dist/lib/branding-core/ai/claude-interpreter.d.ts.map +1 -0
- package/dist/lib/branding-core/ai/claude-interpreter.js +55 -0
- package/dist/lib/branding-core/ai/claude-interpreter.js.map +1 -0
- package/dist/lib/branding-core/ai/intent-applier.d.ts +4 -0
- package/dist/lib/branding-core/ai/intent-applier.d.ts.map +1 -0
- package/dist/lib/branding-core/ai/intent-applier.js +29 -0
- package/dist/lib/branding-core/ai/intent-applier.js.map +1 -0
- package/dist/lib/branding-core/ai/keyword-interpreter.d.ts +4 -0
- package/dist/lib/branding-core/ai/keyword-interpreter.d.ts.map +1 -0
- package/dist/lib/branding-core/ai/keyword-interpreter.js +85 -0
- package/dist/lib/branding-core/ai/keyword-interpreter.js.map +1 -0
- package/dist/lib/branding-core/ai/prompts.d.ts +4 -0
- package/dist/lib/branding-core/ai/prompts.d.ts.map +1 -0
- package/dist/lib/branding-core/ai/prompts.js +79 -0
- package/dist/lib/branding-core/ai/prompts.js.map +1 -0
- package/dist/lib/branding-core/ai/types.d.ts +27 -0
- package/dist/lib/branding-core/ai/types.d.ts.map +1 -0
- package/dist/lib/branding-core/ai/types.js +2 -0
- package/dist/lib/branding-core/ai/types.js.map +1 -0
- package/dist/lib/branding-core/documents/html-generator.d.ts +3 -0
- package/dist/lib/branding-core/documents/html-generator.d.ts.map +1 -0
- package/dist/lib/branding-core/documents/html-generator.js +31 -0
- package/dist/lib/branding-core/documents/html-generator.js.map +1 -0
- package/dist/lib/branding-core/documents/pdf-generator.d.ts +3 -0
- package/dist/lib/branding-core/documents/pdf-generator.d.ts.map +1 -0
- package/dist/lib/branding-core/documents/pdf-generator.js +20 -0
- package/dist/lib/branding-core/documents/pdf-generator.js.map +1 -0
- package/dist/lib/branding-core/exporters/css-variables.d.ts +3 -0
- package/dist/lib/branding-core/exporters/css-variables.d.ts.map +1 -0
- package/dist/lib/branding-core/exporters/css-variables.js +62 -0
- package/dist/lib/branding-core/exporters/css-variables.js.map +1 -0
- package/dist/lib/branding-core/exporters/design-tokens.d.ts +3 -0
- package/dist/lib/branding-core/exporters/design-tokens.d.ts.map +1 -0
- package/dist/lib/branding-core/exporters/design-tokens.js +75 -0
- package/dist/lib/branding-core/exporters/design-tokens.js.map +1 -0
- package/dist/lib/branding-core/exporters/figma-tokens.d.ts +9 -0
- package/dist/lib/branding-core/exporters/figma-tokens.d.ts.map +1 -0
- package/dist/lib/branding-core/exporters/figma-tokens.js +69 -0
- package/dist/lib/branding-core/exporters/figma-tokens.js.map +1 -0
- package/dist/lib/branding-core/exporters/react-theme.d.ts +3 -0
- package/dist/lib/branding-core/exporters/react-theme.d.ts.map +1 -0
- package/dist/lib/branding-core/exporters/react-theme.js +61 -0
- package/dist/lib/branding-core/exporters/react-theme.js.map +1 -0
- package/dist/lib/branding-core/exporters/sass-variables.d.ts +3 -0
- package/dist/lib/branding-core/exporters/sass-variables.d.ts.map +1 -0
- package/dist/lib/branding-core/exporters/sass-variables.js +65 -0
- package/dist/lib/branding-core/exporters/sass-variables.js.map +1 -0
- package/dist/lib/branding-core/exporters/tailwind-preset.d.ts +3 -0
- package/dist/lib/branding-core/exporters/tailwind-preset.d.ts.map +1 -0
- package/dist/lib/branding-core/exporters/tailwind-preset.js +55 -0
- package/dist/lib/branding-core/exporters/tailwind-preset.js.map +1 -0
- package/dist/lib/branding-core/generators/border-system.d.ts +3 -0
- package/dist/lib/branding-core/generators/border-system.d.ts.map +1 -0
- package/dist/lib/branding-core/generators/border-system.js +37 -0
- package/dist/lib/branding-core/generators/border-system.js.map +1 -0
- package/dist/lib/branding-core/generators/color-palette.d.ts +7 -0
- package/dist/lib/branding-core/generators/color-palette.d.ts.map +1 -0
- package/dist/lib/branding-core/generators/color-palette.js +117 -0
- package/dist/lib/branding-core/generators/color-palette.js.map +1 -0
- package/dist/lib/branding-core/generators/favicon-generator.d.ts +3 -0
- package/dist/lib/branding-core/generators/favicon-generator.d.ts.map +1 -0
- package/dist/lib/branding-core/generators/favicon-generator.js +23 -0
- package/dist/lib/branding-core/generators/favicon-generator.js.map +1 -0
- package/dist/lib/branding-core/generators/gradient-system.d.ts +3 -0
- package/dist/lib/branding-core/generators/gradient-system.d.ts.map +1 -0
- package/dist/lib/branding-core/generators/gradient-system.js +74 -0
- package/dist/lib/branding-core/generators/gradient-system.js.map +1 -0
- package/dist/lib/branding-core/generators/logo-generator.d.ts +4 -0
- package/dist/lib/branding-core/generators/logo-generator.d.ts.map +1 -0
- package/dist/lib/branding-core/generators/logo-generator.js +130 -0
- package/dist/lib/branding-core/generators/logo-generator.js.map +1 -0
- package/dist/lib/branding-core/generators/motion-system.d.ts +3 -0
- package/dist/lib/branding-core/generators/motion-system.d.ts.map +1 -0
- package/dist/lib/branding-core/generators/motion-system.js +91 -0
- package/dist/lib/branding-core/generators/motion-system.js.map +1 -0
- package/dist/lib/branding-core/generators/og-image-generator.d.ts +3 -0
- package/dist/lib/branding-core/generators/og-image-generator.d.ts.map +1 -0
- package/dist/lib/branding-core/generators/og-image-generator.js +72 -0
- package/dist/lib/branding-core/generators/og-image-generator.js.map +1 -0
- package/dist/lib/branding-core/generators/shadow-system.d.ts +3 -0
- package/dist/lib/branding-core/generators/shadow-system.d.ts.map +1 -0
- package/dist/lib/branding-core/generators/shadow-system.js +44 -0
- package/dist/lib/branding-core/generators/shadow-system.js.map +1 -0
- package/dist/lib/branding-core/generators/spacing-scale.d.ts +3 -0
- package/dist/lib/branding-core/generators/spacing-scale.d.ts.map +1 -0
- package/dist/lib/branding-core/generators/spacing-scale.js +27 -0
- package/dist/lib/branding-core/generators/spacing-scale.js.map +1 -0
- package/dist/lib/branding-core/generators/typography-system.d.ts +3 -0
- package/dist/lib/branding-core/generators/typography-system.d.ts.map +1 -0
- package/dist/lib/branding-core/generators/typography-system.js +121 -0
- package/dist/lib/branding-core/generators/typography-system.js.map +1 -0
- package/dist/lib/branding-core/index.d.ts +24 -0
- package/dist/lib/branding-core/index.d.ts.map +1 -0
- package/dist/lib/branding-core/index.js +22 -0
- package/dist/lib/branding-core/index.js.map +1 -0
- package/dist/lib/branding-core/validators/brand-consistency.d.ts +3 -0
- package/dist/lib/branding-core/validators/brand-consistency.d.ts.map +1 -0
- package/dist/lib/branding-core/validators/brand-consistency.js +70 -0
- package/dist/lib/branding-core/validators/brand-consistency.js.map +1 -0
- package/dist/lib/branding-core/validators/contrast-checker.d.ts +3 -0
- package/dist/lib/branding-core/validators/contrast-checker.d.ts.map +1 -0
- package/dist/lib/branding-core/validators/contrast-checker.js +33 -0
- package/dist/lib/branding-core/validators/contrast-checker.js.map +1 -0
- package/dist/lib/branding-core/validators/token-schema.d.ts +10 -0
- package/dist/lib/branding-core/validators/token-schema.d.ts.map +1 -0
- package/dist/lib/branding-core/validators/token-schema.js +43 -0
- package/dist/lib/branding-core/validators/token-schema.js.map +1 -0
- package/dist/lib/config.d.ts +7 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +8 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/logger.d.ts +3 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +10 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/types.d.ts +208 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/resources/brand-knowledge.d.ts +3 -0
- package/dist/resources/brand-knowledge.d.ts.map +1 -0
- package/dist/resources/brand-knowledge.js +53 -0
- package/dist/resources/brand-knowledge.js.map +1 -0
- package/dist/resources/brand-templates.d.ts +3 -0
- package/dist/resources/brand-templates.d.ts.map +1 -0
- package/dist/resources/brand-templates.js +68 -0
- package/dist/resources/brand-templates.js.map +1 -0
- package/dist/tools/create-brand-guidelines.d.ts +3 -0
- package/dist/tools/create-brand-guidelines.d.ts.map +1 -0
- package/dist/tools/create-brand-guidelines.js +85 -0
- package/dist/tools/create-brand-guidelines.js.map +1 -0
- package/dist/tools/export-design-tokens.d.ts +3 -0
- package/dist/tools/export-design-tokens.d.ts.map +1 -0
- package/dist/tools/export-design-tokens.js +37 -0
- package/dist/tools/export-design-tokens.js.map +1 -0
- package/dist/tools/generate-brand-assets.d.ts +3 -0
- package/dist/tools/generate-brand-assets.d.ts.map +1 -0
- package/dist/tools/generate-brand-assets.js +37 -0
- package/dist/tools/generate-brand-assets.js.map +1 -0
- package/dist/tools/generate-brand-identity.d.ts +3 -0
- package/dist/tools/generate-brand-identity.d.ts.map +1 -0
- package/dist/tools/generate-brand-identity.js +73 -0
- package/dist/tools/generate-brand-identity.js.map +1 -0
- package/dist/tools/generate-color-palette.d.ts +3 -0
- package/dist/tools/generate-color-palette.d.ts.map +1 -0
- package/dist/tools/generate-color-palette.js +33 -0
- package/dist/tools/generate-color-palette.js.map +1 -0
- package/dist/tools/generate-typography-system.d.ts +3 -0
- package/dist/tools/generate-typography-system.d.ts.map +1 -0
- package/dist/tools/generate-typography-system.js +28 -0
- package/dist/tools/generate-typography-system.js.map +1 -0
- package/dist/tools/refine-brand-element.d.ts +3 -0
- package/dist/tools/refine-brand-element.d.ts.map +1 -0
- package/dist/tools/refine-brand-element.js +41 -0
- package/dist/tools/refine-brand-element.js.map +1 -0
- package/dist/tools/validate-brand-consistency.d.ts +3 -0
- package/dist/tools/validate-brand-consistency.d.ts.map +1 -0
- package/dist/tools/validate-brand-consistency.js +25 -0
- package/dist/tools/validate-brand-consistency.js.map +1 -0
- package/docs/API.md +110 -0
- package/docs/DATA_SOURCES.md +69 -0
- package/docs/INTEGRATION.md +58 -0
- package/eslint.config.js +52 -0
- package/jest.config.js +40 -0
- package/package.json +78 -0
- package/src/__tests__/integration/brand-generation.test.ts +84 -0
- package/src/__tests__/integration/mcp-server.test.ts +18 -0
- package/src/__tests__/unit/ai-interpreter.test.ts +172 -0
- package/src/__tests__/unit/border-system.test.ts +77 -0
- package/src/__tests__/unit/color-palette.test.ts +161 -0
- package/src/__tests__/unit/contrast-checker.test.ts +124 -0
- package/src/__tests__/unit/design-tokens.test.ts +184 -0
- package/src/__tests__/unit/favicon-generator.test.ts +80 -0
- package/src/__tests__/unit/gradient-system.test.ts +122 -0
- package/src/__tests__/unit/logo-generator.test.ts +146 -0
- package/src/__tests__/unit/motion-system.test.ts +91 -0
- package/src/__tests__/unit/og-image-generator.test.ts +115 -0
- package/src/__tests__/unit/shadow-system.test.ts +63 -0
- package/src/__tests__/unit/spacing-scale.test.ts +60 -0
- package/src/__tests__/unit/typography-system.test.ts +71 -0
- package/src/index.ts +59 -0
- package/src/lib/branding-core/ai/brand-interpreter.ts +30 -0
- package/src/lib/branding-core/ai/claude-interpreter.ts +76 -0
- package/src/lib/branding-core/ai/intent-applier.ts +59 -0
- package/src/lib/branding-core/ai/keyword-interpreter.ts +95 -0
- package/src/lib/branding-core/ai/prompts.ts +93 -0
- package/src/lib/branding-core/ai/types.ts +36 -0
- package/src/lib/branding-core/documents/html-generator.ts +32 -0
- package/src/lib/branding-core/documents/pdf-generator.ts +21 -0
- package/src/lib/branding-core/exporters/css-variables.ts +71 -0
- package/src/lib/branding-core/exporters/design-tokens.ts +86 -0
- package/src/lib/branding-core/exporters/figma-tokens.ts +87 -0
- package/src/lib/branding-core/exporters/react-theme.ts +69 -0
- package/src/lib/branding-core/exporters/sass-variables.ts +74 -0
- package/src/lib/branding-core/exporters/tailwind-preset.ts +67 -0
- package/src/lib/branding-core/generators/border-system.ts +41 -0
- package/src/lib/branding-core/generators/color-palette.ts +147 -0
- package/src/lib/branding-core/generators/favicon-generator.ts +33 -0
- package/src/lib/branding-core/generators/gradient-system.ts +120 -0
- package/src/lib/branding-core/generators/logo-generator.ts +152 -0
- package/src/lib/branding-core/generators/motion-system.ts +98 -0
- package/src/lib/branding-core/generators/og-image-generator.ts +97 -0
- package/src/lib/branding-core/generators/shadow-system.ts +66 -0
- package/src/lib/branding-core/generators/spacing-scale.ts +29 -0
- package/src/lib/branding-core/generators/typography-system.ts +128 -0
- package/src/lib/branding-core/index.ts +28 -0
- package/src/lib/branding-core/validators/brand-consistency.ts +79 -0
- package/src/lib/branding-core/validators/contrast-checker.ts +37 -0
- package/src/lib/branding-core/validators/token-schema.ts +50 -0
- package/src/lib/config.ts +13 -0
- package/src/lib/logger.ts +12 -0
- package/src/lib/types.ts +236 -0
- package/src/resources/brand-knowledge.ts +60 -0
- package/src/resources/brand-templates.ts +70 -0
- package/src/tools/create-brand-guidelines.ts +94 -0
- package/src/tools/export-design-tokens.ts +52 -0
- package/src/tools/generate-brand-assets.ts +48 -0
- package/src/tools/generate-brand-identity.ts +115 -0
- package/src/tools/generate-color-palette.ts +43 -0
- package/src/tools/generate-typography-system.ts +42 -0
- package/src/tools/refine-brand-element.ts +65 -0
- package/src/tools/validate-brand-consistency.ts +32 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { generateOgImage } from '../../lib/branding-core/generators/og-image-generator.js';
|
|
2
|
+
import { generateColorPalette } from '../../lib/branding-core/generators/color-palette.js';
|
|
3
|
+
import { generateTypographySystem } from '../../lib/branding-core/generators/typography-system.js';
|
|
4
|
+
import { generateSpacingScale } from '../../lib/branding-core/generators/spacing-scale.js';
|
|
5
|
+
import { generateGradientSystem } from '../../lib/branding-core/generators/gradient-system.js';
|
|
6
|
+
import {
|
|
7
|
+
generateSvgLogo,
|
|
8
|
+
defaultLogoConfig,
|
|
9
|
+
} from '../../lib/branding-core/generators/logo-generator.js';
|
|
10
|
+
import type { BrandIdentity } from '../../lib/types.js';
|
|
11
|
+
|
|
12
|
+
function createTestBrand(): BrandIdentity {
|
|
13
|
+
const colors = generateColorPalette('#6B4CE6');
|
|
14
|
+
const typography = generateTypographySystem('sans-serif', 'sans-serif');
|
|
15
|
+
const spacing = generateSpacingScale();
|
|
16
|
+
const gradients = generateGradientSystem(colors, 'minimal');
|
|
17
|
+
const logo = generateSvgLogo({
|
|
18
|
+
...defaultLogoConfig('TestBrand', colors.primary.hex),
|
|
19
|
+
style: 'minimal',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
id: 'test_brand',
|
|
24
|
+
name: 'TestBrand',
|
|
25
|
+
tagline: 'Test tagline',
|
|
26
|
+
industry: 'tech',
|
|
27
|
+
style: 'minimal',
|
|
28
|
+
colors,
|
|
29
|
+
typography,
|
|
30
|
+
spacing,
|
|
31
|
+
gradients,
|
|
32
|
+
logo,
|
|
33
|
+
createdAt: new Date().toISOString(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('generateOgImage', () => {
|
|
38
|
+
const brand = createTestBrand();
|
|
39
|
+
|
|
40
|
+
it('default template produces 1200x630', () => {
|
|
41
|
+
const result = generateOgImage(brand);
|
|
42
|
+
expect(result.width).toBe(1200);
|
|
43
|
+
expect(result.height).toBe(630);
|
|
44
|
+
expect(result.template).toBe('default');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('default template includes brand name', () => {
|
|
48
|
+
const result = generateOgImage(brand);
|
|
49
|
+
expect(result.svg).toContain('TestBrand');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('article template produces 1200x630', () => {
|
|
53
|
+
const result = generateOgImage(brand, 'article', 'My Article');
|
|
54
|
+
expect(result.width).toBe(1200);
|
|
55
|
+
expect(result.height).toBe(630);
|
|
56
|
+
expect(result.template).toBe('article');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('article template includes custom title', () => {
|
|
60
|
+
const result = generateOgImage(brand, 'article', 'My Article', 'Subtitle');
|
|
61
|
+
expect(result.svg).toContain('My Article');
|
|
62
|
+
expect(result.svg).toContain('Subtitle');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('social template produces 1200x1200 square', () => {
|
|
66
|
+
const result = generateOgImage(brand, 'social');
|
|
67
|
+
expect(result.width).toBe(1200);
|
|
68
|
+
expect(result.height).toBe(1200);
|
|
69
|
+
expect(result.template).toBe('social');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('all templates produce valid SVG', () => {
|
|
73
|
+
for (const template of ['default', 'article', 'social'] as const) {
|
|
74
|
+
const result = generateOgImage(brand, template);
|
|
75
|
+
expect(result.svg).toContain('xmlns="http://www.w3.org/2000/svg"');
|
|
76
|
+
expect(result.svg).toContain('</svg>');
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('uses brand font family', () => {
|
|
81
|
+
const result = generateOgImage(brand);
|
|
82
|
+
expect(result.svg).toContain(brand.typography.headingFont);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('default parameters work', () => {
|
|
86
|
+
const result = generateOgImage(brand);
|
|
87
|
+
expect(result.svg).toBeTruthy();
|
|
88
|
+
expect(result.width).toBe(1200);
|
|
89
|
+
expect(result.height).toBe(630);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('uses brand colors', () => {
|
|
93
|
+
const result = generateOgImage(brand, 'social');
|
|
94
|
+
expect(result.svg).toContain(brand.colors.primary.hex);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('works without gradients', () => {
|
|
98
|
+
const brandNoGradients = { ...brand, gradients: undefined };
|
|
99
|
+
const result = generateOgImage(brandNoGradients);
|
|
100
|
+
expect(result.svg).toBeTruthy();
|
|
101
|
+
expect(result.svg).toContain(brand.colors.primary.hex);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('works without logo', () => {
|
|
105
|
+
const brandNoLogo = { ...brand, logo: undefined };
|
|
106
|
+
const result = generateOgImage(brandNoLogo);
|
|
107
|
+
expect(result.svg).toBeTruthy();
|
|
108
|
+
expect(result.svg).toContain('TestBrand');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('social template with custom title', () => {
|
|
112
|
+
const result = generateOgImage(brand, 'social', 'Custom Title');
|
|
113
|
+
expect(result.svg).toContain('Custom Title');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { generateShadowSystem } from '../../lib/branding-core/generators/shadow-system.js';
|
|
2
|
+
|
|
3
|
+
describe('generateShadowSystem', () => {
|
|
4
|
+
it('generates all 6 elevation levels', () => {
|
|
5
|
+
const system = generateShadowSystem();
|
|
6
|
+
const names = Object.keys(system.levels);
|
|
7
|
+
expect(names).toEqual(['none', 'sm', 'md', 'lg', 'xl', '2xl']);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('none level has zero values', () => {
|
|
11
|
+
const system = generateShadowSystem();
|
|
12
|
+
const none = system.levels.none;
|
|
13
|
+
expect(none.blur).toBe(0);
|
|
14
|
+
expect(none.spread).toBe(0);
|
|
15
|
+
expect(none.offsetY).toBe(0);
|
|
16
|
+
expect(none.cssValue).toBe('none');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('blur increases with elevation', () => {
|
|
20
|
+
const system = generateShadowSystem();
|
|
21
|
+
const levels = ['sm', 'md', 'lg', 'xl', '2xl'] as const;
|
|
22
|
+
for (let i = 1; i < levels.length; i++) {
|
|
23
|
+
expect(system.levels[levels[i]].blur).toBeGreaterThan(system.levels[levels[i - 1]].blur);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('uses brand color for tinting', () => {
|
|
28
|
+
const system = generateShadowSystem('#FF0000');
|
|
29
|
+
const md = system.levels.md;
|
|
30
|
+
expect(md.color).toMatch(/^rgba\(/);
|
|
31
|
+
expect(md.cssValue).toContain('rgba(');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('dark theme reverses shadow direction', () => {
|
|
35
|
+
const light = generateShadowSystem('#6B4CE6', 'light');
|
|
36
|
+
const dark = generateShadowSystem('#6B4CE6', 'dark');
|
|
37
|
+
expect(light.levels.md.offsetY).toBeGreaterThan(0);
|
|
38
|
+
expect(dark.levels.md.offsetY).toBeLessThan(0);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('dark theme has higher opacity', () => {
|
|
42
|
+
const light = generateShadowSystem('#6B4CE6', 'light');
|
|
43
|
+
const dark = generateShadowSystem('#6B4CE6', 'dark');
|
|
44
|
+
expect(dark.levels.lg.opacity).toBeGreaterThan(light.levels.lg.opacity);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('generates valid CSS box-shadow strings', () => {
|
|
48
|
+
const system = generateShadowSystem();
|
|
49
|
+
for (const [name, level] of Object.entries(system.levels)) {
|
|
50
|
+
if (name === 'none') {
|
|
51
|
+
expect(level.cssValue).toBe('none');
|
|
52
|
+
} else {
|
|
53
|
+
expect(level.cssValue).toMatch(/\d+px.*rgba\(/);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('works with default parameters', () => {
|
|
59
|
+
const system = generateShadowSystem();
|
|
60
|
+
expect(system.levels).toBeDefined();
|
|
61
|
+
expect(Object.keys(system.levels)).toHaveLength(6);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { generateSpacingScale } from '../../lib/branding-core/generators/spacing-scale.js';
|
|
2
|
+
|
|
3
|
+
describe('generateSpacingScale', () => {
|
|
4
|
+
it('generates scale with default 4px unit', () => {
|
|
5
|
+
const scale = generateSpacingScale();
|
|
6
|
+
expect(scale.unit).toBe(4);
|
|
7
|
+
expect(scale.values['0']).toBe('0px');
|
|
8
|
+
expect(scale.values['1']).toBe('4px');
|
|
9
|
+
expect(scale.values['2']).toBe('8px');
|
|
10
|
+
expect(scale.values['4']).toBe('16px');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('generates scale with custom unit', () => {
|
|
14
|
+
const scale = generateSpacingScale(8);
|
|
15
|
+
expect(scale.unit).toBe(8);
|
|
16
|
+
expect(scale.values['1']).toBe('8px');
|
|
17
|
+
expect(scale.values['2']).toBe('16px');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('includes all standard steps', () => {
|
|
21
|
+
const scale = generateSpacingScale();
|
|
22
|
+
const expectedKeys = [
|
|
23
|
+
'0',
|
|
24
|
+
'0.5',
|
|
25
|
+
'1',
|
|
26
|
+
'1.5',
|
|
27
|
+
'2',
|
|
28
|
+
'2.5',
|
|
29
|
+
'3',
|
|
30
|
+
'4',
|
|
31
|
+
'5',
|
|
32
|
+
'6',
|
|
33
|
+
'8',
|
|
34
|
+
'10',
|
|
35
|
+
'12',
|
|
36
|
+
'16',
|
|
37
|
+
'20',
|
|
38
|
+
'24',
|
|
39
|
+
];
|
|
40
|
+
for (const key of expectedKeys) {
|
|
41
|
+
expect(scale.values[key]).toBeDefined();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('zero spacing is always 0px', () => {
|
|
46
|
+
const scale = generateSpacingScale(8);
|
|
47
|
+
expect(scale.values['0']).toBe('0px');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('values increase monotonically', () => {
|
|
51
|
+
const scale = generateSpacingScale();
|
|
52
|
+
const keys = Object.keys(scale.values)
|
|
53
|
+
.map(Number)
|
|
54
|
+
.sort((a, b) => a - b);
|
|
55
|
+
const values = keys.map((k) => parseInt(scale.values[String(k)]));
|
|
56
|
+
for (let i = 1; i < values.length; i++) {
|
|
57
|
+
expect(values[i]).toBeGreaterThanOrEqual(values[i - 1]);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { generateTypographySystem } from '../../lib/branding-core/generators/typography-system.js';
|
|
2
|
+
|
|
3
|
+
describe('generateTypographySystem', () => {
|
|
4
|
+
it('generates a system with default parameters', () => {
|
|
5
|
+
const system = generateTypographySystem();
|
|
6
|
+
expect(system.headingFont).toBeTruthy();
|
|
7
|
+
expect(system.bodyFont).toBeTruthy();
|
|
8
|
+
expect(system.monoFont).toBe('JetBrains Mono');
|
|
9
|
+
expect(system.baseSize).toBe(16);
|
|
10
|
+
expect(system.steps.length).toBeGreaterThan(0);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('applies correct scale ratio', () => {
|
|
14
|
+
const system = generateTypographySystem('sans-serif', 'serif', 'major-third', 16);
|
|
15
|
+
expect(system.scaleRatio).toBe(1.25);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('uses perfect fourth scale', () => {
|
|
19
|
+
const system = generateTypographySystem('sans-serif', 'serif', 'perfect-fourth');
|
|
20
|
+
expect(system.scaleRatio).toBe(1.333);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('generates correct number of type steps', () => {
|
|
24
|
+
const system = generateTypographySystem();
|
|
25
|
+
expect(system.steps.length).toBe(9);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('base step is at index 2 with baseSize', () => {
|
|
29
|
+
const system = generateTypographySystem('sans-serif', 'serif', 'major-third', 16);
|
|
30
|
+
const baseStep = system.steps[2];
|
|
31
|
+
expect(baseStep.name).toBe('base');
|
|
32
|
+
expect(parseFloat(baseStep.size)).toBeCloseTo(16, 0);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('sizes increase progressively', () => {
|
|
36
|
+
const system = generateTypographySystem();
|
|
37
|
+
const sizes = system.steps.map((s) => parseFloat(s.size));
|
|
38
|
+
for (let i = 1; i < sizes.length; i++) {
|
|
39
|
+
expect(sizes[i]).toBeGreaterThan(sizes[i - 1]);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('respects custom base size', () => {
|
|
44
|
+
const system = generateTypographySystem('sans-serif', 'serif', 'major-third', 18);
|
|
45
|
+
expect(system.baseSize).toBe(18);
|
|
46
|
+
const baseStep = system.steps[2];
|
|
47
|
+
expect(parseFloat(baseStep.size)).toBeCloseTo(18, 0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('generates line heights for all steps', () => {
|
|
51
|
+
const system = generateTypographySystem();
|
|
52
|
+
for (const step of system.steps) {
|
|
53
|
+
expect(parseFloat(step.lineHeight)).toBeGreaterThan(1);
|
|
54
|
+
expect(parseFloat(step.lineHeight)).toBeLessThanOrEqual(1.6);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('generates valid font weights', () => {
|
|
59
|
+
const system = generateTypographySystem();
|
|
60
|
+
for (const step of system.steps) {
|
|
61
|
+
expect([400, 600, 700]).toContain(step.weight);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('pairs serif heading with sans-serif body', () => {
|
|
66
|
+
const system = generateTypographySystem('serif', 'sans-serif');
|
|
67
|
+
expect(system.headingFont).toBeTruthy();
|
|
68
|
+
expect(system.bodyFont).toBeTruthy();
|
|
69
|
+
expect(system.headingFont).not.toBe(system.bodyFont);
|
|
70
|
+
});
|
|
71
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
+
import { loadConfig } from './lib/config.js';
|
|
6
|
+
import { logger } from './lib/logger.js';
|
|
7
|
+
|
|
8
|
+
import { registerGenerateBrandIdentity } from './tools/generate-brand-identity.js';
|
|
9
|
+
import { registerGenerateColorPalette } from './tools/generate-color-palette.js';
|
|
10
|
+
import { registerGenerateTypographySystem } from './tools/generate-typography-system.js';
|
|
11
|
+
import { registerExportDesignTokens } from './tools/export-design-tokens.js';
|
|
12
|
+
import { registerCreateBrandGuidelines } from './tools/create-brand-guidelines.js';
|
|
13
|
+
import { registerValidateBrandConsistency } from './tools/validate-brand-consistency.js';
|
|
14
|
+
import { registerRefineBrandElement } from './tools/refine-brand-element.js';
|
|
15
|
+
import { registerGenerateBrandAssets } from './tools/generate-brand-assets.js';
|
|
16
|
+
|
|
17
|
+
import { registerBrandTemplates } from './resources/brand-templates.js';
|
|
18
|
+
import { registerBrandKnowledge } from './resources/brand-knowledge.js';
|
|
19
|
+
|
|
20
|
+
async function main(): Promise<void> {
|
|
21
|
+
const config = loadConfig();
|
|
22
|
+
logger.info({ env: config.nodeEnv }, 'Starting branding-mcp server');
|
|
23
|
+
|
|
24
|
+
const server = new McpServer({
|
|
25
|
+
name: '@forgespace/branding-mcp',
|
|
26
|
+
version: '0.1.0',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
registerGenerateBrandIdentity(server);
|
|
30
|
+
registerGenerateColorPalette(server);
|
|
31
|
+
registerGenerateTypographySystem(server);
|
|
32
|
+
registerExportDesignTokens(server);
|
|
33
|
+
registerCreateBrandGuidelines(server);
|
|
34
|
+
registerValidateBrandConsistency(server);
|
|
35
|
+
registerRefineBrandElement(server);
|
|
36
|
+
registerGenerateBrandAssets(server);
|
|
37
|
+
|
|
38
|
+
registerBrandTemplates(server);
|
|
39
|
+
registerBrandKnowledge(server);
|
|
40
|
+
|
|
41
|
+
const transport = new StdioServerTransport();
|
|
42
|
+
await server.connect(transport);
|
|
43
|
+
logger.info('Branding MCP server connected via stdio');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
main().catch((error) => {
|
|
47
|
+
logger.fatal(error, 'Failed to start branding-mcp server');
|
|
48
|
+
process.exit(1);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
process.on('SIGTERM', () => {
|
|
52
|
+
logger.info('Received SIGTERM, shutting down');
|
|
53
|
+
process.exit(0);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
process.on('SIGINT', () => {
|
|
57
|
+
logger.info('Received SIGINT, shutting down');
|
|
58
|
+
process.exit(0);
|
|
59
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { BrandIdentity } from '../../types.js';
|
|
2
|
+
import type { BrandIntent, InterpreterOptions } from './types.js';
|
|
3
|
+
import { interpretWithKeywords } from './keyword-interpreter.js';
|
|
4
|
+
import { interpretWithClaude } from './claude-interpreter.js';
|
|
5
|
+
import { logger } from '../../logger.js';
|
|
6
|
+
|
|
7
|
+
export type InterpreterStrategy = 'keyword' | 'ai' | 'auto';
|
|
8
|
+
|
|
9
|
+
export async function interpretFeedback(
|
|
10
|
+
feedback: string,
|
|
11
|
+
element: 'colors' | 'typography' | 'spacing',
|
|
12
|
+
currentBrand: BrandIdentity,
|
|
13
|
+
strategy: InterpreterStrategy = 'auto',
|
|
14
|
+
options?: InterpreterOptions
|
|
15
|
+
): Promise<BrandIntent> {
|
|
16
|
+
const useAi = strategy === 'ai' || (strategy === 'auto' && !!options?.anthropicApiKey);
|
|
17
|
+
|
|
18
|
+
if (useAi && options?.anthropicApiKey) {
|
|
19
|
+
try {
|
|
20
|
+
return await interpretWithClaude(feedback, element, currentBrand, options);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
logger.warn(
|
|
23
|
+
{ error: error instanceof Error ? error.message : String(error) },
|
|
24
|
+
'AI interpretation failed, falling back to keywords'
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return interpretWithKeywords(feedback, element, currentBrand);
|
|
30
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { BrandIdentity } from '../../types.js';
|
|
2
|
+
import type { BrandIntent, InterpreterOptions } from './types.js';
|
|
3
|
+
import { buildRefinementPrompt, buildGenerationPrompt } from './prompts.js';
|
|
4
|
+
import { logger } from '../../logger.js';
|
|
5
|
+
|
|
6
|
+
async function callClaude(prompt: string, options: Required<InterpreterOptions>): Promise<string> {
|
|
7
|
+
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
|
8
|
+
method: 'POST',
|
|
9
|
+
headers: {
|
|
10
|
+
'Content-Type': 'application/json',
|
|
11
|
+
'x-api-key': options.anthropicApiKey,
|
|
12
|
+
'anthropic-version': '2023-06-01',
|
|
13
|
+
},
|
|
14
|
+
body: JSON.stringify({
|
|
15
|
+
model: options.model,
|
|
16
|
+
max_tokens: options.maxTokens,
|
|
17
|
+
messages: [{ role: 'user', content: prompt }],
|
|
18
|
+
}),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
const body = await response.text();
|
|
23
|
+
throw new Error(`Anthropic API error ${response.status}: ${body}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const data = (await response.json()) as {
|
|
27
|
+
content: Array<{ type: string; text: string }>;
|
|
28
|
+
};
|
|
29
|
+
const textBlock = data.content.find((b) => b.type === 'text');
|
|
30
|
+
if (!textBlock) throw new Error('No text content in Anthropic response');
|
|
31
|
+
return textBlock.text;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseIntentJson(raw: string): BrandIntent {
|
|
35
|
+
const jsonMatch = raw.match(/\{[\s\S]*\}/);
|
|
36
|
+
if (!jsonMatch) throw new Error('No JSON object found in AI response');
|
|
37
|
+
return JSON.parse(jsonMatch[0]) as BrandIntent;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function interpretWithClaude(
|
|
41
|
+
feedback: string,
|
|
42
|
+
element: 'colors' | 'typography' | 'spacing',
|
|
43
|
+
currentBrand: BrandIdentity,
|
|
44
|
+
options: InterpreterOptions
|
|
45
|
+
): Promise<BrandIntent> {
|
|
46
|
+
const opts: Required<InterpreterOptions> = {
|
|
47
|
+
anthropicApiKey: options.anthropicApiKey ?? '',
|
|
48
|
+
model: options.model ?? 'claude-sonnet-4-20250514',
|
|
49
|
+
maxTokens: options.maxTokens ?? 512,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const prompt = buildRefinementPrompt(feedback, element, currentBrand);
|
|
53
|
+
logger.info({ element, model: opts.model }, 'Interpreting feedback with Claude');
|
|
54
|
+
|
|
55
|
+
const raw = await callClaude(prompt, opts);
|
|
56
|
+
return parseIntentJson(raw);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function suggestBrandWithClaude(
|
|
60
|
+
brandName: string,
|
|
61
|
+
industry: string,
|
|
62
|
+
description: string,
|
|
63
|
+
options: InterpreterOptions
|
|
64
|
+
): Promise<BrandIntent> {
|
|
65
|
+
const opts: Required<InterpreterOptions> = {
|
|
66
|
+
anthropicApiKey: options.anthropicApiKey ?? '',
|
|
67
|
+
model: options.model ?? 'claude-sonnet-4-20250514',
|
|
68
|
+
maxTokens: options.maxTokens ?? 512,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const prompt = buildGenerationPrompt(brandName, industry, description);
|
|
72
|
+
logger.info({ brandName, model: opts.model }, 'Generating brand suggestions with Claude');
|
|
73
|
+
|
|
74
|
+
const raw = await callClaude(prompt, opts);
|
|
75
|
+
return parseIntentJson(raw);
|
|
76
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { BrandIdentity } from '../../types.js';
|
|
2
|
+
import type { BrandIntent } from './types.js';
|
|
3
|
+
import { generateColorPalette, hexToHsl, hslToHex } from '../generators/color-palette.js';
|
|
4
|
+
import { generateTypographySystem } from '../generators/typography-system.js';
|
|
5
|
+
|
|
6
|
+
function clamp(value: number, min: number, max: number): number {
|
|
7
|
+
return Math.max(min, Math.min(max, value));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function applyColorShifts(
|
|
11
|
+
baseHex: string,
|
|
12
|
+
hueShift: number,
|
|
13
|
+
saturationShift: number,
|
|
14
|
+
lightnessShift: number
|
|
15
|
+
): string {
|
|
16
|
+
const hsl = hexToHsl(baseHex);
|
|
17
|
+
return hslToHex(
|
|
18
|
+
(hsl.h + hueShift + 360) % 360,
|
|
19
|
+
clamp(hsl.s + saturationShift, 0, 100),
|
|
20
|
+
clamp(hsl.l + lightnessShift, 5, 95)
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function applyIntent(brand: BrandIdentity, intent: BrandIntent): BrandIdentity {
|
|
25
|
+
const updated = { ...brand };
|
|
26
|
+
|
|
27
|
+
if (intent.color) {
|
|
28
|
+
const { baseColor, harmony, theme, hueShift, saturationShift, lightnessShift } = intent.color;
|
|
29
|
+
|
|
30
|
+
let effectiveBase = baseColor ?? brand.colors.primary.hex;
|
|
31
|
+
|
|
32
|
+
if (hueShift || saturationShift || lightnessShift) {
|
|
33
|
+
effectiveBase = applyColorShifts(
|
|
34
|
+
effectiveBase,
|
|
35
|
+
hueShift ?? 0,
|
|
36
|
+
saturationShift ?? 0,
|
|
37
|
+
lightnessShift ?? 0
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
updated.colors = generateColorPalette(effectiveBase, harmony ?? 'complementary', theme);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (intent.typography) {
|
|
45
|
+
const { headingCategory, bodyCategory, scaleRatio, baseSize } = intent.typography;
|
|
46
|
+
updated.typography = generateTypographySystem(
|
|
47
|
+
(headingCategory ?? brand.typography.headingFont.includes('serif')) ? 'serif' : 'sans-serif',
|
|
48
|
+
bodyCategory ?? 'sans-serif',
|
|
49
|
+
scaleRatio,
|
|
50
|
+
baseSize ?? brand.typography.baseSize
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (intent.style) {
|
|
55
|
+
updated.style = intent.style;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return updated;
|
|
59
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { BrandIdentity } from '../../types.js';
|
|
2
|
+
import type { BrandIntent, ColorIntent, TypographyIntent } from './types.js';
|
|
3
|
+
|
|
4
|
+
const COLOR_KEYWORDS: Record<string, ColorIntent> = {
|
|
5
|
+
warm: { harmony: 'analogous', hueShift: 15 },
|
|
6
|
+
cool: { harmony: 'analogous', hueShift: -15 },
|
|
7
|
+
subtle: { harmony: 'analogous', saturationShift: -15 },
|
|
8
|
+
vibrant: { harmony: 'triadic', saturationShift: 15 },
|
|
9
|
+
bold: { harmony: 'complementary', saturationShift: 10 },
|
|
10
|
+
muted: { harmony: 'monochromatic', saturationShift: -20 },
|
|
11
|
+
dark: { theme: 'dark', lightnessShift: -15 },
|
|
12
|
+
light: { theme: 'light', lightnessShift: 15 },
|
|
13
|
+
monochromatic: { harmony: 'monochromatic' },
|
|
14
|
+
complementary: { harmony: 'complementary' },
|
|
15
|
+
analogous: { harmony: 'analogous' },
|
|
16
|
+
triadic: { harmony: 'triadic' },
|
|
17
|
+
premium: { harmony: 'analogous', saturationShift: -10, lightnessShift: -10 },
|
|
18
|
+
playful: { harmony: 'triadic', saturationShift: 15, lightnessShift: 5 },
|
|
19
|
+
corporate: { harmony: 'complementary', saturationShift: -10 },
|
|
20
|
+
organic: { harmony: 'analogous', hueShift: 30 },
|
|
21
|
+
elegant: { harmony: 'monochromatic', saturationShift: -15, lightnessShift: -5 },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const TYPOGRAPHY_KEYWORDS: Record<string, TypographyIntent> = {
|
|
25
|
+
serif: { headingCategory: 'serif' },
|
|
26
|
+
'sans-serif': { headingCategory: 'sans-serif' },
|
|
27
|
+
mono: { bodyCategory: 'monospace' },
|
|
28
|
+
monospace: { bodyCategory: 'monospace' },
|
|
29
|
+
display: { headingCategory: 'display' },
|
|
30
|
+
handwritten: { headingCategory: 'handwriting' },
|
|
31
|
+
compact: { scaleRatio: 'minor-second' },
|
|
32
|
+
tight: { scaleRatio: 'major-second' },
|
|
33
|
+
spacious: { scaleRatio: 'perfect-fourth' },
|
|
34
|
+
dramatic: { scaleRatio: 'golden-ratio' },
|
|
35
|
+
larger: { baseSize: 18 },
|
|
36
|
+
smaller: { baseSize: 14 },
|
|
37
|
+
editorial: { headingCategory: 'serif', scaleRatio: 'perfect-fourth' },
|
|
38
|
+
technical: { bodyCategory: 'monospace', headingCategory: 'sans-serif' },
|
|
39
|
+
modern: { headingCategory: 'sans-serif', bodyCategory: 'sans-serif' },
|
|
40
|
+
classic: { headingCategory: 'serif', bodyCategory: 'serif' },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function mergeColorIntents(intents: ColorIntent[]): ColorIntent {
|
|
44
|
+
const merged: ColorIntent = {};
|
|
45
|
+
for (const intent of intents) {
|
|
46
|
+
if (intent.harmony) merged.harmony = intent.harmony;
|
|
47
|
+
if (intent.theme) merged.theme = intent.theme;
|
|
48
|
+
if (intent.baseColor) merged.baseColor = intent.baseColor;
|
|
49
|
+
merged.saturationShift = (merged.saturationShift ?? 0) + (intent.saturationShift ?? 0);
|
|
50
|
+
merged.lightnessShift = (merged.lightnessShift ?? 0) + (intent.lightnessShift ?? 0);
|
|
51
|
+
merged.hueShift = (merged.hueShift ?? 0) + (intent.hueShift ?? 0);
|
|
52
|
+
}
|
|
53
|
+
return merged;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function interpretWithKeywords(
|
|
57
|
+
feedback: string,
|
|
58
|
+
element: 'colors' | 'typography' | 'spacing',
|
|
59
|
+
_currentBrand?: BrandIdentity
|
|
60
|
+
): BrandIntent {
|
|
61
|
+
const lower = feedback.toLowerCase();
|
|
62
|
+
|
|
63
|
+
if (element === 'colors') {
|
|
64
|
+
const matched: ColorIntent[] = [];
|
|
65
|
+
for (const [keyword, intent] of Object.entries(COLOR_KEYWORDS)) {
|
|
66
|
+
if (lower.includes(keyword)) {
|
|
67
|
+
matched.push(intent);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
color: matched.length > 0 ? mergeColorIntents(matched) : { harmony: 'complementary' },
|
|
72
|
+
reasoning: `Keyword match: ${matched.length > 0 ? 'matched color keywords' : 'default fallback'}`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (element === 'typography') {
|
|
77
|
+
const matched: TypographyIntent[] = [];
|
|
78
|
+
for (const [keyword, intent] of Object.entries(TYPOGRAPHY_KEYWORDS)) {
|
|
79
|
+
if (lower.includes(keyword)) {
|
|
80
|
+
matched.push(intent);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const merged: TypographyIntent = {};
|
|
84
|
+
for (const intent of matched) {
|
|
85
|
+
Object.assign(merged, intent);
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
typography:
|
|
89
|
+
Object.keys(merged).length > 0 ? merged : { headingCategory: 'sans-serif' as const },
|
|
90
|
+
reasoning: `Keyword match: ${matched.length > 0 ? 'matched typography keywords' : 'default fallback'}`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { reasoning: 'No actionable keywords found for spacing refinement' };
|
|
95
|
+
}
|