@forgespace/branding-mcp 0.6.2 → 0.7.1
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 +41 -5
- package/dist/index.js +1 -1
- package/package.json +27 -9
- package/server.json +29 -0
- package/.env.example +0 -3
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -22
- package/.github/workflows/ci.yml +0 -73
- package/.github/workflows/release-automation.yml +0 -56
- package/.github/workflows/security-scan.yml +0 -37
- package/.gitleaks.toml +0 -14
- package/.prettierrc.json +0 -10
- package/CHANGELOG.md +0 -92
- package/CONTRIBUTING.md +0 -203
- package/data/README.md +0 -13
- package/docs/API.md +0 -110
- package/docs/DATA_SOURCES.md +0 -69
- package/docs/INTEGRATION.md +0 -58
- package/eslint.config.js +0 -52
- package/jest.config.js +0 -40
- package/src/__tests__/integration/brand-generation.test.ts +0 -84
- package/src/__tests__/integration/mcp-server.test.ts +0 -18
- package/src/__tests__/unit/ai-interpreter.test.ts +0 -172
- package/src/__tests__/unit/border-system.test.ts +0 -77
- package/src/__tests__/unit/color-palette.test.ts +0 -161
- package/src/__tests__/unit/contrast-checker.test.ts +0 -124
- package/src/__tests__/unit/design-system-tool.test.ts +0 -176
- package/src/__tests__/unit/design-tokens.test.ts +0 -184
- package/src/__tests__/unit/favicon-generator.test.ts +0 -80
- package/src/__tests__/unit/gradient-system.test.ts +0 -122
- package/src/__tests__/unit/logo-generator.test.ts +0 -146
- package/src/__tests__/unit/motion-system.test.ts +0 -91
- package/src/__tests__/unit/og-image-generator.test.ts +0 -115
- package/src/__tests__/unit/shadow-system.test.ts +0 -63
- package/src/__tests__/unit/spacing-scale.test.ts +0 -60
- package/src/__tests__/unit/typography-system.test.ts +0 -71
- package/src/index.ts +0 -76
- package/src/lib/branding-core/ai/brand-interpreter.ts +0 -30
- package/src/lib/branding-core/ai/claude-interpreter.ts +0 -76
- package/src/lib/branding-core/ai/intent-applier.ts +0 -59
- package/src/lib/branding-core/ai/keyword-interpreter.ts +0 -95
- package/src/lib/branding-core/ai/prompts.ts +0 -93
- package/src/lib/branding-core/ai/types.ts +0 -36
- package/src/lib/branding-core/documents/html-generator.ts +0 -32
- package/src/lib/branding-core/documents/pdf-generator.ts +0 -21
- package/src/lib/branding-core/exporters/css-variables.ts +0 -71
- package/src/lib/branding-core/exporters/design-tokens.ts +0 -86
- package/src/lib/branding-core/exporters/figma-tokens.ts +0 -87
- package/src/lib/branding-core/exporters/react-theme.ts +0 -69
- package/src/lib/branding-core/exporters/sass-variables.ts +0 -74
- package/src/lib/branding-core/exporters/tailwind-preset.ts +0 -67
- package/src/lib/branding-core/generators/border-system.ts +0 -41
- package/src/lib/branding-core/generators/color-palette.ts +0 -147
- package/src/lib/branding-core/generators/favicon-generator.ts +0 -33
- package/src/lib/branding-core/generators/gradient-system.ts +0 -120
- package/src/lib/branding-core/generators/logo-generator.ts +0 -152
- package/src/lib/branding-core/generators/motion-system.ts +0 -98
- package/src/lib/branding-core/generators/og-image-generator.ts +0 -97
- package/src/lib/branding-core/generators/shadow-system.ts +0 -66
- package/src/lib/branding-core/generators/spacing-scale.ts +0 -29
- package/src/lib/branding-core/generators/typography-system.ts +0 -128
- package/src/lib/branding-core/index.ts +0 -28
- package/src/lib/branding-core/validators/brand-consistency.ts +0 -79
- package/src/lib/branding-core/validators/contrast-checker.ts +0 -37
- package/src/lib/branding-core/validators/token-schema.ts +0 -50
- package/src/lib/config.ts +0 -13
- package/src/lib/logger.ts +0 -12
- package/src/lib/types.ts +0 -236
- package/src/resources/brand-knowledge.ts +0 -60
- package/src/resources/brand-templates.ts +0 -385
- package/src/tools/create-brand-guidelines.ts +0 -94
- package/src/tools/export-design-tokens.ts +0 -52
- package/src/tools/generate-brand-assets.ts +0 -48
- package/src/tools/generate-brand-identity.ts +0 -115
- package/src/tools/generate-color-palette.ts +0 -43
- package/src/tools/generate-design-system.ts +0 -163
- package/src/tools/generate-typography-system.ts +0 -42
- package/src/tools/refine-brand-element.ts +0 -65
- package/src/tools/validate-brand-consistency.ts +0 -32
- package/tsconfig.json +0 -21
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import type { BrandStyle, LogoConfig, LogoOutput } from '../../types.js';
|
|
2
|
-
|
|
3
|
-
export function defaultLogoConfig(brandName: string, primaryColor: string): LogoConfig {
|
|
4
|
-
return {
|
|
5
|
-
text: brandName,
|
|
6
|
-
font: 'Inter',
|
|
7
|
-
fontSize: 48,
|
|
8
|
-
color: primaryColor,
|
|
9
|
-
backgroundColor: 'transparent',
|
|
10
|
-
width: 400,
|
|
11
|
-
height: 120,
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function generateWordmark(config: LogoConfig): string {
|
|
16
|
-
const { text, font, fontSize, color, backgroundColor, width, height } = config;
|
|
17
|
-
const initial = text.charAt(0).toUpperCase();
|
|
18
|
-
const circleR = height * 0.35;
|
|
19
|
-
const circleX = circleR + 20;
|
|
20
|
-
const circleY = height / 2;
|
|
21
|
-
const textX = circleX + circleR + 16;
|
|
22
|
-
const textY = height / 2;
|
|
23
|
-
const fontFamily = `'${font}', sans-serif`;
|
|
24
|
-
|
|
25
|
-
return [
|
|
26
|
-
`<svg xmlns="http://www.w3.org/2000/svg" width="400" height="120" viewBox="0 0 400 120">`,
|
|
27
|
-
backgroundColor !== 'transparent'
|
|
28
|
-
? ` <rect width="${width}" height="${height}" fill="${backgroundColor}" rx="8"/>`
|
|
29
|
-
: '',
|
|
30
|
-
` <circle cx="${circleX}" cy="${circleY}" r="${circleR}" fill="${color}"/>`,
|
|
31
|
-
` <text x="${circleX}" y="${circleY}" fill="white" font-size="${fontSize * 0.7}" font-family="${fontFamily}" font-weight="700" text-anchor="middle" dominant-baseline="central">${initial}</text>`,
|
|
32
|
-
` <text x="${textX}" y="${textY}" fill="${color}" font-size="${fontSize}" font-family="${fontFamily}" font-weight="600" dominant-baseline="central">${text}</text>`,
|
|
33
|
-
'</svg>',
|
|
34
|
-
]
|
|
35
|
-
.filter(Boolean)
|
|
36
|
-
.join('\n');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const MONOGRAM_SHAPES: Record<BrandStyle, string> = {
|
|
40
|
-
minimal: 'roundedSquare',
|
|
41
|
-
bold: 'hexagon',
|
|
42
|
-
elegant: 'thinCircle',
|
|
43
|
-
playful: 'blob',
|
|
44
|
-
corporate: 'rectangle',
|
|
45
|
-
tech: 'diamond',
|
|
46
|
-
organic: 'ellipse',
|
|
47
|
-
retro: 'octagon',
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
function monogramContainer(style: BrandStyle, color: string): string {
|
|
51
|
-
const shape = MONOGRAM_SHAPES[style] ?? 'thinCircle';
|
|
52
|
-
const containers: Record<string, string> = {
|
|
53
|
-
roundedSquare: `<rect x="10" y="10" width="100" height="100" rx="16" fill="${color}"/>`,
|
|
54
|
-
hexagon: `<polygon points="60,5 110,30 110,90 60,115 10,90 10,30" fill="${color}"/>`,
|
|
55
|
-
thinCircle: `<circle cx="60" cy="60" r="50" fill="none" stroke="${color}" stroke-width="3"/>`,
|
|
56
|
-
blob: `<ellipse cx="60" cy="60" rx="52" ry="48" fill="${color}"/>`,
|
|
57
|
-
rectangle: `<rect x="10" y="15" width="100" height="90" rx="4" fill="${color}"/>`,
|
|
58
|
-
diamond: `<polygon points="60,5 115,60 60,115 5,60" fill="${color}"/>`,
|
|
59
|
-
ellipse: `<ellipse cx="60" cy="60" rx="55" ry="45" fill="${color}"/>`,
|
|
60
|
-
octagon: `<polygon points="35,5 85,5 115,35 115,85 85,115 35,115 5,85 5,35" fill="${color}"/>`,
|
|
61
|
-
};
|
|
62
|
-
return containers[shape] ?? containers.thinCircle;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function generateMonogram(config: LogoConfig): string {
|
|
66
|
-
const { text, font, color } = config;
|
|
67
|
-
const initial = text.charAt(0).toUpperCase();
|
|
68
|
-
const style = config.style ?? 'minimal';
|
|
69
|
-
const fontFamily = `'${font}', sans-serif`;
|
|
70
|
-
const isThinCircle = MONOGRAM_SHAPES[style] === 'thinCircle';
|
|
71
|
-
const textColor = isThinCircle ? color : 'white';
|
|
72
|
-
|
|
73
|
-
return [
|
|
74
|
-
`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120">`,
|
|
75
|
-
` ${monogramContainer(style, color)}`,
|
|
76
|
-
` <text x="60" y="60" fill="${textColor}" font-size="56" font-family="${fontFamily}" font-weight="700" text-anchor="middle" dominant-baseline="central">${initial}</text>`,
|
|
77
|
-
'</svg>',
|
|
78
|
-
].join('\n');
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const ABSTRACT_BUILDERS: Record<BrandStyle, (c: string) => string> = {
|
|
82
|
-
minimal: (c) =>
|
|
83
|
-
` <circle cx="40" cy="60" r="30" fill="${c}" opacity="0.8"/>` +
|
|
84
|
-
`\n <circle cx="60" cy="40" r="30" fill="${c}" opacity="0.5"/>` +
|
|
85
|
-
`\n <circle cx="80" cy="60" r="30" fill="${c}" opacity="0.3"/>`,
|
|
86
|
-
bold: (c) =>
|
|
87
|
-
` <rect x="10" y="10" width="50" height="50" fill="${c}" opacity="0.9"/>` +
|
|
88
|
-
`\n <rect x="40" y="40" width="50" height="50" fill="${c}" opacity="0.6"/>` +
|
|
89
|
-
`\n <rect x="60" y="20" width="40" height="40" fill="${c}" opacity="0.3"/>`,
|
|
90
|
-
elegant: (c) =>
|
|
91
|
-
` <path d="M60,10 A50,50 0 0,1 110,60" fill="none" stroke="${c}" stroke-width="2"/>` +
|
|
92
|
-
`\n <path d="M60,25 A35,35 0 0,1 95,60" fill="none" stroke="${c}" stroke-width="2"/>` +
|
|
93
|
-
`\n <path d="M60,40 A20,20 0 0,1 80,60" fill="none" stroke="${c}" stroke-width="2"/>`,
|
|
94
|
-
playful: (c) =>
|
|
95
|
-
` <circle cx="30" cy="40" r="8" fill="${c}"/>` +
|
|
96
|
-
`\n <circle cx="60" cy="25" r="12" fill="${c}" opacity="0.7"/>` +
|
|
97
|
-
`\n <circle cx="90" cy="50" r="10" fill="${c}" opacity="0.5"/>` +
|
|
98
|
-
`\n <circle cx="50" cy="80" r="14" fill="${c}" opacity="0.6"/>`,
|
|
99
|
-
corporate: (c) =>
|
|
100
|
-
` <rect x="10" y="10" width="30" height="100" fill="${c}" opacity="0.3"/>` +
|
|
101
|
-
`\n <rect x="45" y="10" width="30" height="100" fill="${c}" opacity="0.5"/>` +
|
|
102
|
-
`\n <rect x="80" y="10" width="30" height="100" fill="${c}" opacity="0.7"/>`,
|
|
103
|
-
tech: (c) =>
|
|
104
|
-
` <line x1="10" y1="60" x2="50" y2="30" stroke="${c}" stroke-width="2"/>` +
|
|
105
|
-
`\n <line x1="50" y1="30" x2="90" y2="50" stroke="${c}" stroke-width="2"/>` +
|
|
106
|
-
`\n <line x1="90" y1="50" x2="110" y2="20" stroke="${c}" stroke-width="2"/>` +
|
|
107
|
-
`\n <circle cx="50" cy="30" r="4" fill="${c}"/>` +
|
|
108
|
-
`\n <circle cx="90" cy="50" r="4" fill="${c}"/>`,
|
|
109
|
-
organic: (c) =>
|
|
110
|
-
` <path d="M10,60 Q30,20 60,60 Q90,100 110,60" fill="none" stroke="${c}" stroke-width="3"/>` +
|
|
111
|
-
`\n <path d="M10,80 Q30,40 60,80 Q90,120 110,80" fill="none" stroke="${c}" stroke-width="2" opacity="0.5"/>`,
|
|
112
|
-
retro: (c) =>
|
|
113
|
-
` <polygon points="60,10 75,35 110,35 82,55 93,85 60,67 27,85 38,55 10,35 45,35" fill="${c}"/>` +
|
|
114
|
-
`\n <polygon points="60,25 70,42 90,42 74,53 80,70 60,60 40,70 46,53 30,42 50,42" fill="white" opacity="0.3"/>`,
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
function generateAbstract(config: LogoConfig): string {
|
|
118
|
-
const { color } = config;
|
|
119
|
-
const style = config.style ?? 'minimal';
|
|
120
|
-
const builder = ABSTRACT_BUILDERS[style] ?? ABSTRACT_BUILDERS.minimal;
|
|
121
|
-
|
|
122
|
-
return [
|
|
123
|
-
`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120">`,
|
|
124
|
-
builder(color),
|
|
125
|
-
'</svg>',
|
|
126
|
-
].join('\n');
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function generateIcon(config: LogoConfig): string {
|
|
130
|
-
const { text, font, color } = config;
|
|
131
|
-
const initial = text.charAt(0).toUpperCase();
|
|
132
|
-
const fontFamily = `'${font}', sans-serif`;
|
|
133
|
-
|
|
134
|
-
return [
|
|
135
|
-
`<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">`,
|
|
136
|
-
` <circle cx="32" cy="32" r="28" fill="${color}"/>`,
|
|
137
|
-
` <text x="32" y="32" fill="white" font-size="32" font-family="${fontFamily}" font-weight="700" text-anchor="middle" dominant-baseline="central">${initial}</text>`,
|
|
138
|
-
'</svg>',
|
|
139
|
-
].join('\n');
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function generateSvgLogo(config: LogoConfig): LogoOutput {
|
|
143
|
-
const wordmark = generateWordmark(config);
|
|
144
|
-
const monogram = generateMonogram(config);
|
|
145
|
-
const abstract = generateAbstract(config);
|
|
146
|
-
const icon = generateIcon(config);
|
|
147
|
-
|
|
148
|
-
return {
|
|
149
|
-
svg: wordmark,
|
|
150
|
-
variants: { wordmark, monogram, abstract, icon },
|
|
151
|
-
};
|
|
152
|
-
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import type { BrandStyle, DurationName, EasingName, MotionSystem } from '../../types.js';
|
|
2
|
-
|
|
3
|
-
const STYLE_DURATIONS: Record<BrandStyle, Record<DurationName, number>> = {
|
|
4
|
-
minimal: { instant: 0, fast: 100, normal: 200, slow: 300, slower: 400 },
|
|
5
|
-
bold: { instant: 0, fast: 100, normal: 200, slow: 300, slower: 450 },
|
|
6
|
-
elegant: { instant: 0, fast: 200, normal: 350, slow: 500, slower: 700 },
|
|
7
|
-
playful: { instant: 0, fast: 150, normal: 250, slow: 400, slower: 600 },
|
|
8
|
-
corporate: { instant: 0, fast: 120, normal: 200, slow: 300, slower: 400 },
|
|
9
|
-
tech: { instant: 0, fast: 80, normal: 150, slow: 250, slower: 350 },
|
|
10
|
-
organic: { instant: 0, fast: 180, normal: 300, slow: 450, slower: 650 },
|
|
11
|
-
retro: { instant: 0, fast: 150, normal: 250, slow: 350, slower: 500 },
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const STYLE_EASINGS: Record<BrandStyle, Record<EasingName, string>> = {
|
|
15
|
-
minimal: {
|
|
16
|
-
'ease-in': 'cubic-bezier(0.4, 0, 1, 1)',
|
|
17
|
-
'ease-out': 'cubic-bezier(0, 0, 0.2, 1)',
|
|
18
|
-
'ease-in-out': 'cubic-bezier(0.4, 0, 0.2, 1)',
|
|
19
|
-
spring: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
|
|
20
|
-
bounce: 'cubic-bezier(0.34, 1.2, 0.64, 1)',
|
|
21
|
-
},
|
|
22
|
-
bold: {
|
|
23
|
-
'ease-in': 'cubic-bezier(0.5, 0, 1, 1)',
|
|
24
|
-
'ease-out': 'cubic-bezier(0, 0, 0.15, 1)',
|
|
25
|
-
'ease-in-out': 'cubic-bezier(0.5, 0, 0.15, 1)',
|
|
26
|
-
spring: 'cubic-bezier(0.22, 1.8, 0.36, 1)',
|
|
27
|
-
bounce: 'cubic-bezier(0.22, 1.5, 0.36, 1)',
|
|
28
|
-
},
|
|
29
|
-
elegant: {
|
|
30
|
-
'ease-in': 'cubic-bezier(0.42, 0, 1, 1)',
|
|
31
|
-
'ease-out': 'cubic-bezier(0, 0, 0.58, 1)',
|
|
32
|
-
'ease-in-out': 'cubic-bezier(0.42, 0, 0.58, 1)',
|
|
33
|
-
spring: 'cubic-bezier(0.25, 1.2, 0.5, 1)',
|
|
34
|
-
bounce: 'cubic-bezier(0.25, 1.1, 0.5, 1)',
|
|
35
|
-
},
|
|
36
|
-
playful: {
|
|
37
|
-
'ease-in': 'cubic-bezier(0.4, 0, 1, 1)',
|
|
38
|
-
'ease-out': 'cubic-bezier(0, 0, 0.2, 1)',
|
|
39
|
-
'ease-in-out': 'cubic-bezier(0.4, 0, 0.2, 1)',
|
|
40
|
-
spring: 'cubic-bezier(0.18, 2.0, 0.4, 1)',
|
|
41
|
-
bounce: 'cubic-bezier(0.18, 1.8, 0.4, 1)',
|
|
42
|
-
},
|
|
43
|
-
corporate: {
|
|
44
|
-
'ease-in': 'cubic-bezier(0.4, 0, 1, 1)',
|
|
45
|
-
'ease-out': 'cubic-bezier(0, 0, 0.2, 1)',
|
|
46
|
-
'ease-in-out': 'cubic-bezier(0.4, 0, 0.2, 1)',
|
|
47
|
-
spring: 'cubic-bezier(0.3, 1.3, 0.6, 1)',
|
|
48
|
-
bounce: 'cubic-bezier(0.3, 1.15, 0.6, 1)',
|
|
49
|
-
},
|
|
50
|
-
tech: {
|
|
51
|
-
'ease-in': 'cubic-bezier(0.55, 0, 1, 1)',
|
|
52
|
-
'ease-out': 'cubic-bezier(0, 0, 0.1, 1)',
|
|
53
|
-
'ease-in-out': 'cubic-bezier(0.55, 0, 0.1, 1)',
|
|
54
|
-
spring: 'cubic-bezier(0.2, 1.6, 0.4, 1)',
|
|
55
|
-
bounce: 'cubic-bezier(0.2, 1.4, 0.4, 1)',
|
|
56
|
-
},
|
|
57
|
-
organic: {
|
|
58
|
-
'ease-in': 'cubic-bezier(0.35, 0, 0.9, 1)',
|
|
59
|
-
'ease-out': 'cubic-bezier(0.1, 0, 0.3, 1)',
|
|
60
|
-
'ease-in-out': 'cubic-bezier(0.35, 0, 0.3, 1)',
|
|
61
|
-
spring: 'cubic-bezier(0.28, 1.4, 0.5, 1)',
|
|
62
|
-
bounce: 'cubic-bezier(0.28, 1.25, 0.5, 1)',
|
|
63
|
-
},
|
|
64
|
-
retro: {
|
|
65
|
-
'ease-in': 'cubic-bezier(0.5, 0, 1, 1)',
|
|
66
|
-
'ease-out': 'cubic-bezier(0, 0, 0.3, 1)',
|
|
67
|
-
'ease-in-out': 'cubic-bezier(0.5, 0, 0.3, 1)',
|
|
68
|
-
spring: 'cubic-bezier(0.3, 1.5, 0.5, 1)',
|
|
69
|
-
bounce: 'cubic-bezier(0.3, 1.3, 0.5, 1)',
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
function buildTransitions(
|
|
74
|
-
durations: Record<DurationName, number>,
|
|
75
|
-
defaultEasing: string
|
|
76
|
-
): Record<string, string> {
|
|
77
|
-
return {
|
|
78
|
-
fade: `opacity ${durations.normal}ms ${defaultEasing}`,
|
|
79
|
-
slide: `transform ${durations.normal}ms ${defaultEasing}`,
|
|
80
|
-
scale: `transform ${durations.fast}ms ${defaultEasing}`,
|
|
81
|
-
color: `color ${durations.slow}ms ${defaultEasing}, background-color ${durations.slow}ms ${defaultEasing}`,
|
|
82
|
-
all: `all ${durations.normal}ms ${defaultEasing}`,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function generateMotionSystem(style: BrandStyle = 'minimal'): MotionSystem {
|
|
87
|
-
const durationValues = STYLE_DURATIONS[style];
|
|
88
|
-
const easingValues = STYLE_EASINGS[style];
|
|
89
|
-
const durations = {} as Record<DurationName, string>;
|
|
90
|
-
for (const [name, ms] of Object.entries(durationValues)) {
|
|
91
|
-
durations[name as DurationName] = `${ms}ms`;
|
|
92
|
-
}
|
|
93
|
-
return {
|
|
94
|
-
durations,
|
|
95
|
-
easings: easingValues,
|
|
96
|
-
transitions: buildTransitions(durationValues, easingValues['ease-out']),
|
|
97
|
-
};
|
|
98
|
-
}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import type { BrandIdentity, OgImageOutput, OgTemplate } from '../../types.js';
|
|
2
|
-
|
|
3
|
-
function getGradientColors(brand: BrandIdentity): [string, string] {
|
|
4
|
-
return [brand.colors.primary.hex, brand.colors.secondary.hex];
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
function stripSvgWrapper(svg: string): string {
|
|
8
|
-
return svg.replace(/<svg[^>]*>/, '').replace(/<\/svg>/, '');
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function buildSvg(
|
|
12
|
-
w: number,
|
|
13
|
-
h: number,
|
|
14
|
-
colors: [string, string],
|
|
15
|
-
font: string,
|
|
16
|
-
title: string,
|
|
17
|
-
subtitle: string,
|
|
18
|
-
logoSection: string
|
|
19
|
-
): string {
|
|
20
|
-
return [
|
|
21
|
-
`<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">`,
|
|
22
|
-
` <defs>`,
|
|
23
|
-
` <style>@import url('https://fonts.googleapis.com/css2?family=${encodeURIComponent(font)}');</style>`,
|
|
24
|
-
` <linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">`,
|
|
25
|
-
` <stop offset="0%" style="stop-color:${colors[0]}"/>`,
|
|
26
|
-
` <stop offset="100%" style="stop-color:${colors[1]}"/>`,
|
|
27
|
-
` </linearGradient>`,
|
|
28
|
-
` </defs>`,
|
|
29
|
-
` <rect width="${w}" height="${h}" fill="url(#bg)"/>`,
|
|
30
|
-
logoSection,
|
|
31
|
-
` <text x="${w / 2}" y="${h / 2 - 20}" fill="white" font-size="56" font-family="'${font}', sans-serif" font-weight="700" text-anchor="middle" dominant-baseline="central">${title}</text>`,
|
|
32
|
-
subtitle
|
|
33
|
-
? ` <text x="${w / 2}" y="${h / 2 + 40}" fill="white" font-size="28" font-family="'${font}', sans-serif" font-weight="400" text-anchor="middle" dominant-baseline="central" opacity="0.8">${subtitle}</text>`
|
|
34
|
-
: '',
|
|
35
|
-
'</svg>',
|
|
36
|
-
]
|
|
37
|
-
.filter(Boolean)
|
|
38
|
-
.join('\n');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function buildDefaultTemplate(brand: BrandIdentity, title?: string, subtitle?: string): string {
|
|
42
|
-
const displayTitle = title ?? brand.name;
|
|
43
|
-
const displaySub = subtitle ?? brand.tagline ?? '';
|
|
44
|
-
const font = brand.typography.headingFont;
|
|
45
|
-
const logoSvg = brand.logo?.variants?.icon ?? '';
|
|
46
|
-
const logoSection = logoSvg
|
|
47
|
-
? `<g transform="translate(540, 80) scale(1.5)">${stripSvgWrapper(logoSvg)}</g>`
|
|
48
|
-
: '';
|
|
49
|
-
|
|
50
|
-
return buildSvg(1200, 630, getGradientColors(brand), font, displayTitle, displaySub, logoSection);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function buildArticleTemplate(brand: BrandIdentity, title?: string, subtitle?: string): string {
|
|
54
|
-
const displayTitle = title ?? 'Untitled Article';
|
|
55
|
-
const displaySub = subtitle ?? '';
|
|
56
|
-
const font = brand.typography.headingFont;
|
|
57
|
-
const logoSvg = brand.logo?.variants?.icon ?? '';
|
|
58
|
-
const logoSection = logoSvg
|
|
59
|
-
? `<g transform="translate(1080, 30) scale(0.8)">${stripSvgWrapper(logoSvg)}</g>`
|
|
60
|
-
: '';
|
|
61
|
-
|
|
62
|
-
return buildSvg(1200, 630, getGradientColors(brand), font, displayTitle, displaySub, logoSection);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function buildSocialTemplate(brand: BrandIdentity, title?: string): string {
|
|
66
|
-
const displayTitle = title ?? brand.name;
|
|
67
|
-
const font = brand.typography.headingFont;
|
|
68
|
-
const colors = getGradientColors(brand);
|
|
69
|
-
const logoSvg = brand.logo?.variants?.icon ?? '';
|
|
70
|
-
const logoSection = logoSvg
|
|
71
|
-
? `<g transform="translate(536, 300) scale(2)">${stripSvgWrapper(logoSvg)}</g>`
|
|
72
|
-
: '';
|
|
73
|
-
|
|
74
|
-
return buildSvg(1200, 1200, colors, font, displayTitle, '', logoSection);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function generateOgImage(
|
|
78
|
-
brand: BrandIdentity,
|
|
79
|
-
template: OgTemplate = 'default',
|
|
80
|
-
title?: string,
|
|
81
|
-
subtitle?: string
|
|
82
|
-
): OgImageOutput {
|
|
83
|
-
const builders: Record<OgTemplate, () => string> = {
|
|
84
|
-
default: () => buildDefaultTemplate(brand, title, subtitle),
|
|
85
|
-
article: () => buildArticleTemplate(brand, title, subtitle),
|
|
86
|
-
social: () => buildSocialTemplate(brand, title),
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
const svg = builders[template]();
|
|
90
|
-
const dimensions: Record<OgTemplate, { width: number; height: number }> = {
|
|
91
|
-
default: { width: 1200, height: 630 },
|
|
92
|
-
article: { width: 1200, height: 630 },
|
|
93
|
-
social: { width: 1200, height: 1200 },
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
return { template, svg, ...dimensions[template] };
|
|
97
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
BrandStyle,
|
|
3
|
-
ColorTheme,
|
|
4
|
-
ShadowLevel,
|
|
5
|
-
ShadowLevelName,
|
|
6
|
-
ShadowSystem,
|
|
7
|
-
} from '../../types.js';
|
|
8
|
-
import { hexToHsl, hslToHex } from './color-palette.js';
|
|
9
|
-
|
|
10
|
-
const LEVEL_CONFIGS: Record<
|
|
11
|
-
ShadowLevelName,
|
|
12
|
-
{ y: number; blur: number; spread: number; opacity: number }
|
|
13
|
-
> = {
|
|
14
|
-
none: { y: 0, blur: 0, spread: 0, opacity: 0 },
|
|
15
|
-
sm: { y: 1, blur: 2, spread: 0, opacity: 0.05 },
|
|
16
|
-
md: { y: 2, blur: 4, spread: -1, opacity: 0.08 },
|
|
17
|
-
lg: { y: 4, blur: 8, spread: -2, opacity: 0.1 },
|
|
18
|
-
xl: { y: 8, blur: 16, spread: -4, opacity: 0.12 },
|
|
19
|
-
'2xl': { y: 16, blur: 32, spread: -8, opacity: 0.15 },
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
function makeShadowColor(primaryHex: string, opacity: number): string {
|
|
23
|
-
const hsl = hexToHsl(primaryHex);
|
|
24
|
-
const tinted = hslToHex(hsl.h, Math.min(hsl.s, 30), 20);
|
|
25
|
-
const r = parseInt(tinted.slice(1, 3), 16);
|
|
26
|
-
const g = parseInt(tinted.slice(3, 5), 16);
|
|
27
|
-
const b = parseInt(tinted.slice(5, 7), 16);
|
|
28
|
-
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function buildLevel(
|
|
32
|
-
cfg: (typeof LEVEL_CONFIGS)[ShadowLevelName],
|
|
33
|
-
primaryHex: string,
|
|
34
|
-
dark: boolean
|
|
35
|
-
): ShadowLevel {
|
|
36
|
-
const sign = dark ? -1 : 1;
|
|
37
|
-
const adjustedOpacity = dark ? cfg.opacity * 1.5 : cfg.opacity;
|
|
38
|
-
const color = makeShadowColor(primaryHex, adjustedOpacity);
|
|
39
|
-
const oY = cfg.y * sign;
|
|
40
|
-
const cssValue =
|
|
41
|
-
cfg.blur === 0
|
|
42
|
-
? 'none'
|
|
43
|
-
: `${cfg.y === 0 ? 0 : `${oY}px`} ${oY === 0 ? '' : `${Math.abs(oY)}px `}${cfg.blur}px ${cfg.spread}px ${color}`.trim();
|
|
44
|
-
return {
|
|
45
|
-
offsetX: 0,
|
|
46
|
-
offsetY: oY,
|
|
47
|
-
blur: cfg.blur,
|
|
48
|
-
spread: cfg.spread,
|
|
49
|
-
color,
|
|
50
|
-
opacity: adjustedOpacity,
|
|
51
|
-
cssValue,
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function generateShadowSystem(
|
|
56
|
-
primaryHex = '#6B4CE6',
|
|
57
|
-
theme: ColorTheme = 'light',
|
|
58
|
-
_style?: BrandStyle
|
|
59
|
-
): ShadowSystem {
|
|
60
|
-
const dark = theme === 'dark';
|
|
61
|
-
const levels = {} as Record<ShadowLevelName, ShadowLevel>;
|
|
62
|
-
for (const [name, cfg] of Object.entries(LEVEL_CONFIGS)) {
|
|
63
|
-
levels[name as ShadowLevelName] = buildLevel(cfg, primaryHex, dark);
|
|
64
|
-
}
|
|
65
|
-
return { levels };
|
|
66
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { SpacingScale } from '../../types.js';
|
|
2
|
-
|
|
3
|
-
const SPACING_NAMES = [
|
|
4
|
-
'0',
|
|
5
|
-
'0.5',
|
|
6
|
-
'1',
|
|
7
|
-
'1.5',
|
|
8
|
-
'2',
|
|
9
|
-
'2.5',
|
|
10
|
-
'3',
|
|
11
|
-
'4',
|
|
12
|
-
'5',
|
|
13
|
-
'6',
|
|
14
|
-
'8',
|
|
15
|
-
'10',
|
|
16
|
-
'12',
|
|
17
|
-
'16',
|
|
18
|
-
'20',
|
|
19
|
-
'24',
|
|
20
|
-
];
|
|
21
|
-
|
|
22
|
-
export function generateSpacingScale(baseUnit = 4): SpacingScale {
|
|
23
|
-
const values: Record<string, string> = {};
|
|
24
|
-
for (const name of SPACING_NAMES) {
|
|
25
|
-
const multiplier = parseFloat(name);
|
|
26
|
-
values[name] = `${multiplier * baseUnit}px`;
|
|
27
|
-
}
|
|
28
|
-
return { unit: baseUnit, values };
|
|
29
|
-
}
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import type { FontCategory, TypeScaleRatio, TypeStep, TypographySystem } from '../../types.js';
|
|
2
|
-
|
|
3
|
-
const SCALE_RATIOS: Record<TypeScaleRatio, number> = {
|
|
4
|
-
'minor-second': 1.067,
|
|
5
|
-
'major-second': 1.125,
|
|
6
|
-
'minor-third': 1.2,
|
|
7
|
-
'major-third': 1.25,
|
|
8
|
-
'perfect-fourth': 1.333,
|
|
9
|
-
'augmented-fourth': 1.414,
|
|
10
|
-
'perfect-fifth': 1.5,
|
|
11
|
-
'golden-ratio': 1.618,
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const FONT_PAIRINGS: Record<FontCategory, Record<FontCategory, [string, string][]>> = {
|
|
15
|
-
'sans-serif': {
|
|
16
|
-
serif: [
|
|
17
|
-
['Inter', 'Merriweather'],
|
|
18
|
-
['Helvetica Neue', 'Georgia'],
|
|
19
|
-
['Open Sans', 'Lora'],
|
|
20
|
-
],
|
|
21
|
-
'sans-serif': [
|
|
22
|
-
['Montserrat', 'Open Sans'],
|
|
23
|
-
['Poppins', 'Inter'],
|
|
24
|
-
['Raleway', 'Nunito'],
|
|
25
|
-
],
|
|
26
|
-
monospace: [['Inter', 'JetBrains Mono']],
|
|
27
|
-
display: [['Inter', 'Playfair Display']],
|
|
28
|
-
handwriting: [['Inter', 'Caveat']],
|
|
29
|
-
},
|
|
30
|
-
serif: {
|
|
31
|
-
'sans-serif': [
|
|
32
|
-
['Playfair Display', 'Source Sans 3'],
|
|
33
|
-
['Merriweather', 'Open Sans'],
|
|
34
|
-
['Lora', 'Inter'],
|
|
35
|
-
],
|
|
36
|
-
serif: [
|
|
37
|
-
['Playfair Display', 'Merriweather'],
|
|
38
|
-
['Lora', 'Georgia'],
|
|
39
|
-
],
|
|
40
|
-
monospace: [['Merriweather', 'Fira Code']],
|
|
41
|
-
display: [['Merriweather', 'Playfair Display']],
|
|
42
|
-
handwriting: [['Merriweather', 'Caveat']],
|
|
43
|
-
},
|
|
44
|
-
monospace: {
|
|
45
|
-
'sans-serif': [['JetBrains Mono', 'Inter']],
|
|
46
|
-
serif: [['Fira Code', 'Merriweather']],
|
|
47
|
-
monospace: [['JetBrains Mono', 'Fira Code']],
|
|
48
|
-
display: [['JetBrains Mono', 'Playfair Display']],
|
|
49
|
-
handwriting: [['JetBrains Mono', 'Caveat']],
|
|
50
|
-
},
|
|
51
|
-
display: {
|
|
52
|
-
'sans-serif': [
|
|
53
|
-
['Playfair Display', 'Inter'],
|
|
54
|
-
['Bebas Neue', 'Open Sans'],
|
|
55
|
-
],
|
|
56
|
-
serif: [['Playfair Display', 'Merriweather']],
|
|
57
|
-
monospace: [['Playfair Display', 'JetBrains Mono']],
|
|
58
|
-
display: [['Bebas Neue', 'Playfair Display']],
|
|
59
|
-
handwriting: [['Playfair Display', 'Caveat']],
|
|
60
|
-
},
|
|
61
|
-
handwriting: {
|
|
62
|
-
'sans-serif': [['Caveat', 'Inter']],
|
|
63
|
-
serif: [['Caveat', 'Merriweather']],
|
|
64
|
-
monospace: [['Caveat', 'JetBrains Mono']],
|
|
65
|
-
display: [['Caveat', 'Playfair Display']],
|
|
66
|
-
handwriting: [['Caveat', 'Dancing Script']],
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const STEP_NAMES = ['xs', 'sm', 'base', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl'];
|
|
71
|
-
|
|
72
|
-
function pickPairing(headingCat: FontCategory, bodyCat: FontCategory): [string, string] {
|
|
73
|
-
const options = FONT_PAIRINGS[headingCat]?.[bodyCat];
|
|
74
|
-
if (!options?.length) return ['Inter', 'Inter'];
|
|
75
|
-
return options[Math.floor(Math.random() * options.length)];
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function lineHeightForSize(size: number): string {
|
|
79
|
-
if (size <= 16) return '1.6';
|
|
80
|
-
if (size <= 24) return '1.5';
|
|
81
|
-
if (size <= 36) return '1.3';
|
|
82
|
-
return '1.2';
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function letterSpacingForSize(size: number): string {
|
|
86
|
-
if (size <= 14) return '0.02em';
|
|
87
|
-
if (size <= 20) return '0em';
|
|
88
|
-
return '-0.01em';
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function weightForStep(index: number, totalSteps: number): number {
|
|
92
|
-
const mid = Math.floor(totalSteps / 2);
|
|
93
|
-
if (index <= mid) return 400;
|
|
94
|
-
if (index <= mid + 2) return 600;
|
|
95
|
-
return 700;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function generateTypographySystem(
|
|
99
|
-
headingCategory: FontCategory = 'sans-serif',
|
|
100
|
-
bodyCategory: FontCategory = 'serif',
|
|
101
|
-
scaleRatio: TypeScaleRatio = 'major-third',
|
|
102
|
-
baseSize = 16
|
|
103
|
-
): TypographySystem {
|
|
104
|
-
const ratio = SCALE_RATIOS[scaleRatio];
|
|
105
|
-
const [headingFont, bodyFont] = pickPairing(headingCategory, bodyCategory);
|
|
106
|
-
|
|
107
|
-
const baseIndex = 2;
|
|
108
|
-
const steps: TypeStep[] = STEP_NAMES.map((name, i) => {
|
|
109
|
-
const exponent = i - baseIndex;
|
|
110
|
-
const size = Math.round(baseSize * Math.pow(ratio, exponent) * 100) / 100;
|
|
111
|
-
return {
|
|
112
|
-
name,
|
|
113
|
-
size: `${size}px`,
|
|
114
|
-
lineHeight: lineHeightForSize(size),
|
|
115
|
-
letterSpacing: letterSpacingForSize(size),
|
|
116
|
-
weight: weightForStep(i, STEP_NAMES.length),
|
|
117
|
-
};
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
headingFont,
|
|
122
|
-
bodyFont,
|
|
123
|
-
monoFont: 'JetBrains Mono',
|
|
124
|
-
baseSize,
|
|
125
|
-
scaleRatio: ratio,
|
|
126
|
-
steps,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
export {
|
|
2
|
-
generateColorPalette,
|
|
3
|
-
checkContrast,
|
|
4
|
-
hslToHex,
|
|
5
|
-
hexToHsl,
|
|
6
|
-
} from './generators/color-palette.js';
|
|
7
|
-
export { generateTypographySystem } from './generators/typography-system.js';
|
|
8
|
-
export { generateSpacingScale } from './generators/spacing-scale.js';
|
|
9
|
-
export { generateShadowSystem } from './generators/shadow-system.js';
|
|
10
|
-
export { generateBorderSystem } from './generators/border-system.js';
|
|
11
|
-
export { generateMotionSystem } from './generators/motion-system.js';
|
|
12
|
-
export { generateSvgLogo, defaultLogoConfig } from './generators/logo-generator.js';
|
|
13
|
-
export { generateGradientSystem } from './generators/gradient-system.js';
|
|
14
|
-
export { generateFavicons } from './generators/favicon-generator.js';
|
|
15
|
-
export { generateOgImage } from './generators/og-image-generator.js';
|
|
16
|
-
export { exportDesignTokens } from './exporters/design-tokens.js';
|
|
17
|
-
export { exportCssVariables } from './exporters/css-variables.js';
|
|
18
|
-
export { exportTailwindPreset } from './exporters/tailwind-preset.js';
|
|
19
|
-
export { exportFigmaTokens } from './exporters/figma-tokens.js';
|
|
20
|
-
export { exportReactTheme } from './exporters/react-theme.js';
|
|
21
|
-
export { exportSassVariables } from './exporters/sass-variables.js';
|
|
22
|
-
export { validateBrandConsistency } from './validators/brand-consistency.js';
|
|
23
|
-
export { validateContrast } from './validators/contrast-checker.js';
|
|
24
|
-
export { interpretFeedback } from './ai/brand-interpreter.js';
|
|
25
|
-
export { interpretWithKeywords } from './ai/keyword-interpreter.js';
|
|
26
|
-
export { applyIntent } from './ai/intent-applier.js';
|
|
27
|
-
export type { BrandIntent, ColorIntent, TypographyIntent, InterpreterOptions } from './ai/types.js';
|
|
28
|
-
export type { InterpreterStrategy } from './ai/brand-interpreter.js';
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import type { BrandIdentity, BrandValidationIssue, BrandValidationResult } from '../../types.js';
|
|
2
|
-
import { validateContrast } from './contrast-checker.js';
|
|
3
|
-
|
|
4
|
-
function validateColorCompleteness(brand: BrandIdentity): BrandValidationIssue[] {
|
|
5
|
-
const issues: BrandValidationIssue[] = [];
|
|
6
|
-
const { colors } = brand;
|
|
7
|
-
|
|
8
|
-
if (!colors.primary.hex) {
|
|
9
|
-
issues.push({
|
|
10
|
-
severity: 'error',
|
|
11
|
-
element: 'color.primary',
|
|
12
|
-
message: 'Primary color is missing',
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
if (!colors.secondary.hex) {
|
|
16
|
-
issues.push({
|
|
17
|
-
severity: 'error',
|
|
18
|
-
element: 'color.secondary',
|
|
19
|
-
message: 'Secondary color is missing',
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
if (colors.neutral.length < 4) {
|
|
23
|
-
issues.push({
|
|
24
|
-
severity: 'warning',
|
|
25
|
-
element: 'color.neutral',
|
|
26
|
-
message: `Only ${colors.neutral.length} neutral shades defined (recommend 6-10)`,
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return issues;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function validateTypography(brand: BrandIdentity): BrandValidationIssue[] {
|
|
34
|
-
const issues: BrandValidationIssue[] = [];
|
|
35
|
-
const { typography } = brand;
|
|
36
|
-
|
|
37
|
-
if (!typography.headingFont) {
|
|
38
|
-
issues.push({
|
|
39
|
-
severity: 'error',
|
|
40
|
-
element: 'typography.headingFont',
|
|
41
|
-
message: 'Heading font is missing',
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
if (!typography.bodyFont) {
|
|
45
|
-
issues.push({
|
|
46
|
-
severity: 'error',
|
|
47
|
-
element: 'typography.bodyFont',
|
|
48
|
-
message: 'Body font is missing',
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
if (typography.baseSize < 14 || typography.baseSize > 20) {
|
|
52
|
-
issues.push({
|
|
53
|
-
severity: 'warning',
|
|
54
|
-
element: 'typography.baseSize',
|
|
55
|
-
message: `Base size ${typography.baseSize}px is outside recommended range (14-20px)`,
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return issues;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function validateBrandConsistency(brand: BrandIdentity): BrandValidationResult {
|
|
63
|
-
const issues: BrandValidationIssue[] = [
|
|
64
|
-
...validateColorCompleteness(brand),
|
|
65
|
-
...validateContrast(brand),
|
|
66
|
-
...validateTypography(brand),
|
|
67
|
-
];
|
|
68
|
-
|
|
69
|
-
const errorCount = issues.filter((i) => i.severity === 'error').length;
|
|
70
|
-
const warningCount = issues.filter((i) => i.severity === 'warning').length;
|
|
71
|
-
const maxScore = 100;
|
|
72
|
-
const score = Math.max(0, maxScore - errorCount * 20 - warningCount * 5);
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
valid: errorCount === 0,
|
|
76
|
-
score,
|
|
77
|
-
issues,
|
|
78
|
-
};
|
|
79
|
-
}
|