@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.
Files changed (244) hide show
  1. package/.env.example +3 -0
  2. package/.github/PULL_REQUEST_TEMPLATE.md +22 -0
  3. package/.github/workflows/ci.yml +73 -0
  4. package/.github/workflows/release-automation.yml +56 -0
  5. package/.github/workflows/security-scan.yml +37 -0
  6. package/.gitleaks.toml +14 -0
  7. package/.prettierrc.json +10 -0
  8. package/CHANGELOG.md +66 -0
  9. package/CONTRIBUTING.md +203 -0
  10. package/LICENSE +21 -0
  11. package/README.md +105 -0
  12. package/data/README.md +13 -0
  13. package/dist/index.d.ts +3 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +49 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/lib/branding-core/ai/brand-interpreter.d.ts +5 -0
  18. package/dist/lib/branding-core/ai/brand-interpreter.d.ts.map +1 -0
  19. package/dist/lib/branding-core/ai/brand-interpreter.js +16 -0
  20. package/dist/lib/branding-core/ai/brand-interpreter.js.map +1 -0
  21. package/dist/lib/branding-core/ai/claude-interpreter.d.ts +5 -0
  22. package/dist/lib/branding-core/ai/claude-interpreter.d.ts.map +1 -0
  23. package/dist/lib/branding-core/ai/claude-interpreter.js +55 -0
  24. package/dist/lib/branding-core/ai/claude-interpreter.js.map +1 -0
  25. package/dist/lib/branding-core/ai/intent-applier.d.ts +4 -0
  26. package/dist/lib/branding-core/ai/intent-applier.d.ts.map +1 -0
  27. package/dist/lib/branding-core/ai/intent-applier.js +29 -0
  28. package/dist/lib/branding-core/ai/intent-applier.js.map +1 -0
  29. package/dist/lib/branding-core/ai/keyword-interpreter.d.ts +4 -0
  30. package/dist/lib/branding-core/ai/keyword-interpreter.d.ts.map +1 -0
  31. package/dist/lib/branding-core/ai/keyword-interpreter.js +85 -0
  32. package/dist/lib/branding-core/ai/keyword-interpreter.js.map +1 -0
  33. package/dist/lib/branding-core/ai/prompts.d.ts +4 -0
  34. package/dist/lib/branding-core/ai/prompts.d.ts.map +1 -0
  35. package/dist/lib/branding-core/ai/prompts.js +79 -0
  36. package/dist/lib/branding-core/ai/prompts.js.map +1 -0
  37. package/dist/lib/branding-core/ai/types.d.ts +27 -0
  38. package/dist/lib/branding-core/ai/types.d.ts.map +1 -0
  39. package/dist/lib/branding-core/ai/types.js +2 -0
  40. package/dist/lib/branding-core/ai/types.js.map +1 -0
  41. package/dist/lib/branding-core/documents/html-generator.d.ts +3 -0
  42. package/dist/lib/branding-core/documents/html-generator.d.ts.map +1 -0
  43. package/dist/lib/branding-core/documents/html-generator.js +31 -0
  44. package/dist/lib/branding-core/documents/html-generator.js.map +1 -0
  45. package/dist/lib/branding-core/documents/pdf-generator.d.ts +3 -0
  46. package/dist/lib/branding-core/documents/pdf-generator.d.ts.map +1 -0
  47. package/dist/lib/branding-core/documents/pdf-generator.js +20 -0
  48. package/dist/lib/branding-core/documents/pdf-generator.js.map +1 -0
  49. package/dist/lib/branding-core/exporters/css-variables.d.ts +3 -0
  50. package/dist/lib/branding-core/exporters/css-variables.d.ts.map +1 -0
  51. package/dist/lib/branding-core/exporters/css-variables.js +62 -0
  52. package/dist/lib/branding-core/exporters/css-variables.js.map +1 -0
  53. package/dist/lib/branding-core/exporters/design-tokens.d.ts +3 -0
  54. package/dist/lib/branding-core/exporters/design-tokens.d.ts.map +1 -0
  55. package/dist/lib/branding-core/exporters/design-tokens.js +75 -0
  56. package/dist/lib/branding-core/exporters/design-tokens.js.map +1 -0
  57. package/dist/lib/branding-core/exporters/figma-tokens.d.ts +9 -0
  58. package/dist/lib/branding-core/exporters/figma-tokens.d.ts.map +1 -0
  59. package/dist/lib/branding-core/exporters/figma-tokens.js +69 -0
  60. package/dist/lib/branding-core/exporters/figma-tokens.js.map +1 -0
  61. package/dist/lib/branding-core/exporters/react-theme.d.ts +3 -0
  62. package/dist/lib/branding-core/exporters/react-theme.d.ts.map +1 -0
  63. package/dist/lib/branding-core/exporters/react-theme.js +61 -0
  64. package/dist/lib/branding-core/exporters/react-theme.js.map +1 -0
  65. package/dist/lib/branding-core/exporters/sass-variables.d.ts +3 -0
  66. package/dist/lib/branding-core/exporters/sass-variables.d.ts.map +1 -0
  67. package/dist/lib/branding-core/exporters/sass-variables.js +65 -0
  68. package/dist/lib/branding-core/exporters/sass-variables.js.map +1 -0
  69. package/dist/lib/branding-core/exporters/tailwind-preset.d.ts +3 -0
  70. package/dist/lib/branding-core/exporters/tailwind-preset.d.ts.map +1 -0
  71. package/dist/lib/branding-core/exporters/tailwind-preset.js +55 -0
  72. package/dist/lib/branding-core/exporters/tailwind-preset.js.map +1 -0
  73. package/dist/lib/branding-core/generators/border-system.d.ts +3 -0
  74. package/dist/lib/branding-core/generators/border-system.d.ts.map +1 -0
  75. package/dist/lib/branding-core/generators/border-system.js +37 -0
  76. package/dist/lib/branding-core/generators/border-system.js.map +1 -0
  77. package/dist/lib/branding-core/generators/color-palette.d.ts +7 -0
  78. package/dist/lib/branding-core/generators/color-palette.d.ts.map +1 -0
  79. package/dist/lib/branding-core/generators/color-palette.js +117 -0
  80. package/dist/lib/branding-core/generators/color-palette.js.map +1 -0
  81. package/dist/lib/branding-core/generators/favicon-generator.d.ts +3 -0
  82. package/dist/lib/branding-core/generators/favicon-generator.d.ts.map +1 -0
  83. package/dist/lib/branding-core/generators/favicon-generator.js +23 -0
  84. package/dist/lib/branding-core/generators/favicon-generator.js.map +1 -0
  85. package/dist/lib/branding-core/generators/gradient-system.d.ts +3 -0
  86. package/dist/lib/branding-core/generators/gradient-system.d.ts.map +1 -0
  87. package/dist/lib/branding-core/generators/gradient-system.js +74 -0
  88. package/dist/lib/branding-core/generators/gradient-system.js.map +1 -0
  89. package/dist/lib/branding-core/generators/logo-generator.d.ts +4 -0
  90. package/dist/lib/branding-core/generators/logo-generator.d.ts.map +1 -0
  91. package/dist/lib/branding-core/generators/logo-generator.js +130 -0
  92. package/dist/lib/branding-core/generators/logo-generator.js.map +1 -0
  93. package/dist/lib/branding-core/generators/motion-system.d.ts +3 -0
  94. package/dist/lib/branding-core/generators/motion-system.d.ts.map +1 -0
  95. package/dist/lib/branding-core/generators/motion-system.js +91 -0
  96. package/dist/lib/branding-core/generators/motion-system.js.map +1 -0
  97. package/dist/lib/branding-core/generators/og-image-generator.d.ts +3 -0
  98. package/dist/lib/branding-core/generators/og-image-generator.d.ts.map +1 -0
  99. package/dist/lib/branding-core/generators/og-image-generator.js +72 -0
  100. package/dist/lib/branding-core/generators/og-image-generator.js.map +1 -0
  101. package/dist/lib/branding-core/generators/shadow-system.d.ts +3 -0
  102. package/dist/lib/branding-core/generators/shadow-system.d.ts.map +1 -0
  103. package/dist/lib/branding-core/generators/shadow-system.js +44 -0
  104. package/dist/lib/branding-core/generators/shadow-system.js.map +1 -0
  105. package/dist/lib/branding-core/generators/spacing-scale.d.ts +3 -0
  106. package/dist/lib/branding-core/generators/spacing-scale.d.ts.map +1 -0
  107. package/dist/lib/branding-core/generators/spacing-scale.js +27 -0
  108. package/dist/lib/branding-core/generators/spacing-scale.js.map +1 -0
  109. package/dist/lib/branding-core/generators/typography-system.d.ts +3 -0
  110. package/dist/lib/branding-core/generators/typography-system.d.ts.map +1 -0
  111. package/dist/lib/branding-core/generators/typography-system.js +121 -0
  112. package/dist/lib/branding-core/generators/typography-system.js.map +1 -0
  113. package/dist/lib/branding-core/index.d.ts +24 -0
  114. package/dist/lib/branding-core/index.d.ts.map +1 -0
  115. package/dist/lib/branding-core/index.js +22 -0
  116. package/dist/lib/branding-core/index.js.map +1 -0
  117. package/dist/lib/branding-core/validators/brand-consistency.d.ts +3 -0
  118. package/dist/lib/branding-core/validators/brand-consistency.d.ts.map +1 -0
  119. package/dist/lib/branding-core/validators/brand-consistency.js +70 -0
  120. package/dist/lib/branding-core/validators/brand-consistency.js.map +1 -0
  121. package/dist/lib/branding-core/validators/contrast-checker.d.ts +3 -0
  122. package/dist/lib/branding-core/validators/contrast-checker.d.ts.map +1 -0
  123. package/dist/lib/branding-core/validators/contrast-checker.js +33 -0
  124. package/dist/lib/branding-core/validators/contrast-checker.js.map +1 -0
  125. package/dist/lib/branding-core/validators/token-schema.d.ts +10 -0
  126. package/dist/lib/branding-core/validators/token-schema.d.ts.map +1 -0
  127. package/dist/lib/branding-core/validators/token-schema.js +43 -0
  128. package/dist/lib/branding-core/validators/token-schema.js.map +1 -0
  129. package/dist/lib/config.d.ts +7 -0
  130. package/dist/lib/config.d.ts.map +1 -0
  131. package/dist/lib/config.js +8 -0
  132. package/dist/lib/config.js.map +1 -0
  133. package/dist/lib/logger.d.ts +3 -0
  134. package/dist/lib/logger.d.ts.map +1 -0
  135. package/dist/lib/logger.js +10 -0
  136. package/dist/lib/logger.js.map +1 -0
  137. package/dist/lib/types.d.ts +208 -0
  138. package/dist/lib/types.d.ts.map +1 -0
  139. package/dist/lib/types.js +2 -0
  140. package/dist/lib/types.js.map +1 -0
  141. package/dist/resources/brand-knowledge.d.ts +3 -0
  142. package/dist/resources/brand-knowledge.d.ts.map +1 -0
  143. package/dist/resources/brand-knowledge.js +53 -0
  144. package/dist/resources/brand-knowledge.js.map +1 -0
  145. package/dist/resources/brand-templates.d.ts +3 -0
  146. package/dist/resources/brand-templates.d.ts.map +1 -0
  147. package/dist/resources/brand-templates.js +68 -0
  148. package/dist/resources/brand-templates.js.map +1 -0
  149. package/dist/tools/create-brand-guidelines.d.ts +3 -0
  150. package/dist/tools/create-brand-guidelines.d.ts.map +1 -0
  151. package/dist/tools/create-brand-guidelines.js +85 -0
  152. package/dist/tools/create-brand-guidelines.js.map +1 -0
  153. package/dist/tools/export-design-tokens.d.ts +3 -0
  154. package/dist/tools/export-design-tokens.d.ts.map +1 -0
  155. package/dist/tools/export-design-tokens.js +37 -0
  156. package/dist/tools/export-design-tokens.js.map +1 -0
  157. package/dist/tools/generate-brand-assets.d.ts +3 -0
  158. package/dist/tools/generate-brand-assets.d.ts.map +1 -0
  159. package/dist/tools/generate-brand-assets.js +37 -0
  160. package/dist/tools/generate-brand-assets.js.map +1 -0
  161. package/dist/tools/generate-brand-identity.d.ts +3 -0
  162. package/dist/tools/generate-brand-identity.d.ts.map +1 -0
  163. package/dist/tools/generate-brand-identity.js +73 -0
  164. package/dist/tools/generate-brand-identity.js.map +1 -0
  165. package/dist/tools/generate-color-palette.d.ts +3 -0
  166. package/dist/tools/generate-color-palette.d.ts.map +1 -0
  167. package/dist/tools/generate-color-palette.js +33 -0
  168. package/dist/tools/generate-color-palette.js.map +1 -0
  169. package/dist/tools/generate-typography-system.d.ts +3 -0
  170. package/dist/tools/generate-typography-system.d.ts.map +1 -0
  171. package/dist/tools/generate-typography-system.js +28 -0
  172. package/dist/tools/generate-typography-system.js.map +1 -0
  173. package/dist/tools/refine-brand-element.d.ts +3 -0
  174. package/dist/tools/refine-brand-element.d.ts.map +1 -0
  175. package/dist/tools/refine-brand-element.js +41 -0
  176. package/dist/tools/refine-brand-element.js.map +1 -0
  177. package/dist/tools/validate-brand-consistency.d.ts +3 -0
  178. package/dist/tools/validate-brand-consistency.d.ts.map +1 -0
  179. package/dist/tools/validate-brand-consistency.js +25 -0
  180. package/dist/tools/validate-brand-consistency.js.map +1 -0
  181. package/docs/API.md +110 -0
  182. package/docs/DATA_SOURCES.md +69 -0
  183. package/docs/INTEGRATION.md +58 -0
  184. package/eslint.config.js +52 -0
  185. package/jest.config.js +40 -0
  186. package/package.json +78 -0
  187. package/src/__tests__/integration/brand-generation.test.ts +84 -0
  188. package/src/__tests__/integration/mcp-server.test.ts +18 -0
  189. package/src/__tests__/unit/ai-interpreter.test.ts +172 -0
  190. package/src/__tests__/unit/border-system.test.ts +77 -0
  191. package/src/__tests__/unit/color-palette.test.ts +161 -0
  192. package/src/__tests__/unit/contrast-checker.test.ts +124 -0
  193. package/src/__tests__/unit/design-tokens.test.ts +184 -0
  194. package/src/__tests__/unit/favicon-generator.test.ts +80 -0
  195. package/src/__tests__/unit/gradient-system.test.ts +122 -0
  196. package/src/__tests__/unit/logo-generator.test.ts +146 -0
  197. package/src/__tests__/unit/motion-system.test.ts +91 -0
  198. package/src/__tests__/unit/og-image-generator.test.ts +115 -0
  199. package/src/__tests__/unit/shadow-system.test.ts +63 -0
  200. package/src/__tests__/unit/spacing-scale.test.ts +60 -0
  201. package/src/__tests__/unit/typography-system.test.ts +71 -0
  202. package/src/index.ts +59 -0
  203. package/src/lib/branding-core/ai/brand-interpreter.ts +30 -0
  204. package/src/lib/branding-core/ai/claude-interpreter.ts +76 -0
  205. package/src/lib/branding-core/ai/intent-applier.ts +59 -0
  206. package/src/lib/branding-core/ai/keyword-interpreter.ts +95 -0
  207. package/src/lib/branding-core/ai/prompts.ts +93 -0
  208. package/src/lib/branding-core/ai/types.ts +36 -0
  209. package/src/lib/branding-core/documents/html-generator.ts +32 -0
  210. package/src/lib/branding-core/documents/pdf-generator.ts +21 -0
  211. package/src/lib/branding-core/exporters/css-variables.ts +71 -0
  212. package/src/lib/branding-core/exporters/design-tokens.ts +86 -0
  213. package/src/lib/branding-core/exporters/figma-tokens.ts +87 -0
  214. package/src/lib/branding-core/exporters/react-theme.ts +69 -0
  215. package/src/lib/branding-core/exporters/sass-variables.ts +74 -0
  216. package/src/lib/branding-core/exporters/tailwind-preset.ts +67 -0
  217. package/src/lib/branding-core/generators/border-system.ts +41 -0
  218. package/src/lib/branding-core/generators/color-palette.ts +147 -0
  219. package/src/lib/branding-core/generators/favicon-generator.ts +33 -0
  220. package/src/lib/branding-core/generators/gradient-system.ts +120 -0
  221. package/src/lib/branding-core/generators/logo-generator.ts +152 -0
  222. package/src/lib/branding-core/generators/motion-system.ts +98 -0
  223. package/src/lib/branding-core/generators/og-image-generator.ts +97 -0
  224. package/src/lib/branding-core/generators/shadow-system.ts +66 -0
  225. package/src/lib/branding-core/generators/spacing-scale.ts +29 -0
  226. package/src/lib/branding-core/generators/typography-system.ts +128 -0
  227. package/src/lib/branding-core/index.ts +28 -0
  228. package/src/lib/branding-core/validators/brand-consistency.ts +79 -0
  229. package/src/lib/branding-core/validators/contrast-checker.ts +37 -0
  230. package/src/lib/branding-core/validators/token-schema.ts +50 -0
  231. package/src/lib/config.ts +13 -0
  232. package/src/lib/logger.ts +12 -0
  233. package/src/lib/types.ts +236 -0
  234. package/src/resources/brand-knowledge.ts +60 -0
  235. package/src/resources/brand-templates.ts +70 -0
  236. package/src/tools/create-brand-guidelines.ts +94 -0
  237. package/src/tools/export-design-tokens.ts +52 -0
  238. package/src/tools/generate-brand-assets.ts +48 -0
  239. package/src/tools/generate-brand-identity.ts +115 -0
  240. package/src/tools/generate-color-palette.ts +43 -0
  241. package/src/tools/generate-typography-system.ts +42 -0
  242. package/src/tools/refine-brand-element.ts +65 -0
  243. package/src/tools/validate-brand-consistency.ts +32 -0
  244. package/tsconfig.json +21 -0
@@ -0,0 +1,147 @@
1
+ import type {
2
+ ColorHarmony,
3
+ ColorPalette,
4
+ ColorSwatch,
5
+ ColorTheme,
6
+ ContrastResult,
7
+ HslColor,
8
+ } from '../../types.js';
9
+
10
+ const HARMONY_ANGLES: Record<ColorHarmony, number[]> = {
11
+ complementary: [180],
12
+ analogous: [-30, 30],
13
+ triadic: [120, 240],
14
+ 'split-complementary': [150, 210],
15
+ tetradic: [90, 180, 270],
16
+ monochromatic: [0, 0],
17
+ };
18
+
19
+ function hslToHex(h: number, s: number, l: number): string {
20
+ const hNorm = ((h % 360) + 360) % 360;
21
+ const sNorm = s / 100;
22
+ const lNorm = l / 100;
23
+ const a = sNorm * Math.min(lNorm, 1 - lNorm);
24
+ const f = (n: number): string => {
25
+ const k = (n + hNorm / 30) % 12;
26
+ const color = lNorm - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
27
+ return Math.round(255 * color)
28
+ .toString(16)
29
+ .padStart(2, '0');
30
+ };
31
+ return `#${f(0)}${f(8)}${f(4)}`;
32
+ }
33
+
34
+ function hexToHsl(hex: string): HslColor {
35
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
36
+ if (!result) return { h: 0, s: 0, l: 0 };
37
+ const r = parseInt(result[1], 16) / 255;
38
+ const g = parseInt(result[2], 16) / 255;
39
+ const b = parseInt(result[3], 16) / 255;
40
+ const max = Math.max(r, g, b);
41
+ const min = Math.min(r, g, b);
42
+ const l = (max + min) / 2;
43
+ if (max === min) return { h: 0, s: 0, l: Math.round(l * 100) };
44
+ const d = max - min;
45
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
46
+ let h = 0;
47
+ if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
48
+ else if (max === g) h = ((b - r) / d + 2) / 6;
49
+ else h = ((r - g) / d + 4) / 6;
50
+ return {
51
+ h: Math.round(h * 360),
52
+ s: Math.round(s * 100),
53
+ l: Math.round(l * 100),
54
+ };
55
+ }
56
+
57
+ function relativeLuminance(hex: string): number {
58
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
59
+ if (!result) return 0;
60
+ const channels = [result[1], result[2], result[3]].map((c) => {
61
+ const v = parseInt(c, 16) / 255;
62
+ return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
63
+ });
64
+ return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2];
65
+ }
66
+
67
+ export function checkContrast(fg: string, bg: string): ContrastResult {
68
+ const l1 = relativeLuminance(fg);
69
+ const l2 = relativeLuminance(bg);
70
+ const lighter = Math.max(l1, l2);
71
+ const darker = Math.min(l1, l2);
72
+ const ratio = (lighter + 0.05) / (darker + 0.05);
73
+ return {
74
+ ratio: Math.round(ratio * 100) / 100,
75
+ aa: ratio >= 4.5,
76
+ aaLarge: ratio >= 3,
77
+ aaa: ratio >= 7,
78
+ aaaLarge: ratio >= 4.5,
79
+ };
80
+ }
81
+
82
+ function generateNeutrals(baseHue: number, theme: ColorTheme): ColorSwatch[] {
83
+ const steps = [95, 90, 80, 60, 40, 20, 10, 5];
84
+ if (theme === 'dark') steps.reverse();
85
+ return steps.map((l, i) => ({
86
+ name: `neutral-${(i + 1) * 100}`,
87
+ hex: hslToHex(baseHue, 5, l),
88
+ hsl: { h: baseHue, s: 5, l },
89
+ usage: `Neutral shade ${i + 1}`,
90
+ }));
91
+ }
92
+
93
+ function makeSwatch(name: string, h: number, s: number, l: number, usage: string): ColorSwatch {
94
+ return {
95
+ name,
96
+ hex: hslToHex(h, s, l),
97
+ hsl: { h: ((h % 360) + 360) % 360, s, l },
98
+ usage,
99
+ };
100
+ }
101
+
102
+ export function generateColorPalette(
103
+ baseColor?: string,
104
+ harmony: ColorHarmony = 'complementary',
105
+ theme: ColorTheme = 'both'
106
+ ): ColorPalette {
107
+ const base = baseColor ? hexToHsl(baseColor) : { h: 250, s: 65, l: 50 };
108
+ const angles = HARMONY_ANGLES[harmony];
109
+
110
+ const primary = makeSwatch('primary', base.h, base.s, base.l, 'Primary brand color');
111
+ const secondary = makeSwatch(
112
+ 'secondary',
113
+ base.h + (angles[0] ?? 180),
114
+ base.s - 10,
115
+ base.l + 5,
116
+ 'Secondary brand color'
117
+ );
118
+ const accent = makeSwatch(
119
+ 'accent',
120
+ base.h + (angles[1] ?? angles[0] ?? 120),
121
+ Math.min(base.s + 15, 100),
122
+ base.l,
123
+ 'Accent and highlight color'
124
+ );
125
+
126
+ const neutrals = generateNeutrals(base.h, theme);
127
+
128
+ const semantic = {
129
+ success: makeSwatch('success', 142, 70, 45, 'Success states'),
130
+ warning: makeSwatch('warning', 38, 92, 50, 'Warning states'),
131
+ error: makeSwatch('error', 0, 84, 60, 'Error states'),
132
+ info: makeSwatch('info', 210, 79, 56, 'Informational states'),
133
+ };
134
+
135
+ const white = '#ffffff';
136
+ const dark = '#1a1a1a';
137
+ const contrast: Record<string, ContrastResult> = {
138
+ 'primary-on-white': checkContrast(primary.hex, white),
139
+ 'primary-on-dark': checkContrast(primary.hex, dark),
140
+ 'secondary-on-white': checkContrast(secondary.hex, white),
141
+ 'accent-on-white': checkContrast(accent.hex, white),
142
+ };
143
+
144
+ return { primary, secondary, accent, neutral: neutrals, semantic, contrast };
145
+ }
146
+
147
+ export { hslToHex, hexToHsl };
@@ -0,0 +1,33 @@
1
+ import type { FaviconSet, FaviconSize } from '../../types.js';
2
+
3
+ function rescaleSvg(iconSvg: string, size: FaviconSize, brandColor: string): string {
4
+ const strokeScale = size <= 32 ? 2 : 1;
5
+ let svg = iconSvg
6
+ .replace(/width="[^"]*"/, `width="${size}"`)
7
+ .replace(/height="[^"]*"/, `height="${size}"`)
8
+ .replace(/viewBox="[^"]*"/, `viewBox="0 0 64 64"`);
9
+
10
+ if (strokeScale > 1) {
11
+ svg = svg.replace(
12
+ /stroke-width="(\d+)"/g,
13
+ (_, w) => `stroke-width="${Number(w) * strokeScale}"`
14
+ );
15
+ }
16
+
17
+ if (!svg.includes('fill=') && !svg.includes('<circle')) {
18
+ svg = svg.replace('<svg ', `<svg style="color:${brandColor}" `);
19
+ }
20
+
21
+ return svg;
22
+ }
23
+
24
+ export function generateFavicons(iconSvg: string, brandColor: string): FaviconSet {
25
+ const sizes: FaviconSize[] = [16, 32, 180, 512];
26
+ const result = {} as Record<FaviconSize, string>;
27
+
28
+ for (const size of sizes) {
29
+ result[size] = rescaleSvg(iconSvg, size, brandColor);
30
+ }
31
+
32
+ return { sizes: result };
33
+ }
@@ -0,0 +1,120 @@
1
+ import type {
2
+ BrandStyle,
3
+ ColorPalette,
4
+ Gradient,
5
+ GradientPresetName,
6
+ GradientStop,
7
+ GradientSystem,
8
+ GradientType,
9
+ HslColor,
10
+ } from '../../types.js';
11
+ import { hexToHsl, hslToHex } from './color-palette.js';
12
+
13
+ interface StyleGradientConfig {
14
+ type: GradientType;
15
+ angle: number;
16
+ stops: number;
17
+ lightnessVariance: number;
18
+ }
19
+
20
+ const STYLE_GRADIENT_CONFIG: Record<BrandStyle, StyleGradientConfig> = {
21
+ minimal: { type: 'linear', angle: 180, stops: 2, lightnessVariance: 5 },
22
+ bold: { type: 'linear', angle: 45, stops: 3, lightnessVariance: 25 },
23
+ elegant: { type: 'linear', angle: 135, stops: 2, lightnessVariance: 10 },
24
+ playful: { type: 'conic', angle: 0, stops: 3, lightnessVariance: 20 },
25
+ corporate: { type: 'linear', angle: 180, stops: 2, lightnessVariance: 8 },
26
+ tech: { type: 'linear', angle: 315, stops: 2, lightnessVariance: 15 },
27
+ organic: { type: 'radial', angle: 0, stops: 3, lightnessVariance: 12 },
28
+ retro: { type: 'linear', angle: 90, stops: 3, lightnessVariance: 18 },
29
+ };
30
+
31
+ function shiftLightness(hsl: HslColor, amount: number): string {
32
+ const l = Math.max(0, Math.min(100, hsl.l + amount));
33
+ return hslToHex(hsl.h, hsl.s, l);
34
+ }
35
+
36
+ function buildStops(
37
+ baseHex: string,
38
+ endHex: string,
39
+ count: number,
40
+ variance: number
41
+ ): GradientStop[] {
42
+ const baseHsl = hexToHsl(baseHex);
43
+ const endHsl = hexToHsl(endHex);
44
+ if (count === 2) {
45
+ return [
46
+ { color: baseHex, position: 0 },
47
+ { color: endHex, position: 100 },
48
+ ];
49
+ }
50
+ const midHex = shiftLightness(
51
+ { h: (baseHsl.h + endHsl.h) / 2, s: baseHsl.s, l: baseHsl.l },
52
+ variance / 2
53
+ );
54
+ return [
55
+ { color: baseHex, position: 0 },
56
+ { color: midHex, position: 50 },
57
+ { color: endHex, position: 100 },
58
+ ];
59
+ }
60
+
61
+ function toCssValue(type: GradientType, angle: number, stops: GradientStop[]): string {
62
+ const stopStr = stops.map((s) => `${s.color} ${s.position}%`).join(', ');
63
+ if (type === 'radial') return `radial-gradient(circle, ${stopStr})`;
64
+ if (type === 'conic') return `conic-gradient(from ${angle}deg, ${stopStr})`;
65
+ return `linear-gradient(${angle}deg, ${stopStr})`;
66
+ }
67
+
68
+ function createGradient(type: GradientType, angle: number, stops: GradientStop[]): Gradient {
69
+ return {
70
+ type,
71
+ angle: type !== 'radial' ? angle : undefined,
72
+ stops,
73
+ cssValue: toCssValue(type, angle, stops),
74
+ };
75
+ }
76
+
77
+ function buildPreset(
78
+ name: GradientPresetName,
79
+ colors: ColorPalette,
80
+ config: StyleGradientConfig
81
+ ): Gradient {
82
+ const { type, angle, stops: count, lightnessVariance } = config;
83
+ const primary = colors.primary.hex;
84
+ const secondary = colors.secondary.hex;
85
+ const accent = colors.accent.hex;
86
+ const neutralLight = colors.neutral[0]?.hex ?? '#f5f5f5';
87
+ const neutralDark = colors.neutral[colors.neutral.length - 1]?.hex ?? '#1a1a1a';
88
+
89
+ const presetMap: Record<GradientPresetName, () => GradientStop[]> = {
90
+ hero: () => buildStops(primary, secondary, count, lightnessVariance),
91
+ button: () => buildStops(accent, primary, count, lightnessVariance),
92
+ card: () =>
93
+ buildStops(
94
+ neutralLight,
95
+ shiftLightness(hexToHsl(neutralLight), -lightnessVariance),
96
+ 2,
97
+ lightnessVariance
98
+ ),
99
+ text: () => buildStops(primary, accent, count, lightnessVariance),
100
+ background: () => buildStops(neutralLight, neutralDark, 2, lightnessVariance),
101
+ };
102
+
103
+ const stops = presetMap[name]();
104
+ return createGradient(type, angle, stops);
105
+ }
106
+
107
+ export function generateGradientSystem(
108
+ colors: ColorPalette,
109
+ style: BrandStyle = 'minimal'
110
+ ): GradientSystem {
111
+ const config = STYLE_GRADIENT_CONFIG[style];
112
+ const presetNames: GradientPresetName[] = ['hero', 'button', 'card', 'text', 'background'];
113
+
114
+ const presets = {} as Record<GradientPresetName, Gradient>;
115
+ for (const name of presetNames) {
116
+ presets[name] = buildPreset(name, colors, config);
117
+ }
118
+
119
+ return { presets };
120
+ }
@@ -0,0 +1,152 @@
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
+ }
@@ -0,0 +1,98 @@
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
+ }
@@ -0,0 +1,97 @@
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
+ }