From 7e3ac86c4825252fd6e95e2c94784cb980fbee31 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 12 Aug 2026 16:13:39 -0700 Subject: [PATCH 1/4] improvement(emails): size the header wordmark to the landing navbar's rule The email header rendered the wordmark at 34px of ink against 16px body copy, and inked it #1a1a1a while every other line of the email uses #434343. Size it by the rule the landing navbar states: the mark stands 2px above and below its neighbouring text (18px against 14px chip labels), so 20px against the email's 16px body copy. Rasterize it from the same brand outlines the navbar renders, filled with the email's own textBody token, so the two surfaces cannot drift. --- .../components/sim-wordmark/sim-wordmark.tsx | 15 +++-- .../emails/components/email-layout.tsx | 19 ++++-- apps/sim/lib/branding/index.ts | 6 ++ apps/sim/lib/branding/wordmark.test.ts | 49 +++++++++++++++ apps/sim/lib/branding/wordmark.ts | 30 +++++++++ .../sim/public/brand/color/email/wordmark.png | Bin 10275 -> 2538 bytes apps/sim/scripts/generate-email-wordmark.ts | 59 ++++++++++++++++++ 7 files changed, 166 insertions(+), 12 deletions(-) create mode 100644 apps/sim/lib/branding/wordmark.test.ts create mode 100644 apps/sim/lib/branding/wordmark.ts create mode 100644 apps/sim/scripts/generate-email-wordmark.ts diff --git a/apps/sim/app/(landing)/components/navbar/components/sim-wordmark/sim-wordmark.tsx b/apps/sim/app/(landing)/components/navbar/components/sim-wordmark/sim-wordmark.tsx index 0043641a3e0..37c883552dd 100644 --- a/apps/sim/app/(landing)/components/navbar/components/sim-wordmark/sim-wordmark.tsx +++ b/apps/sim/app/(landing)/components/navbar/components/sim-wordmark/sim-wordmark.tsx @@ -1,7 +1,11 @@ +import { WORDMARK_PATHS, WORDMARK_VIEW_BOX } from '@/lib/branding/wordmark' + /** * Inline "sim" brand logotype (wordmark, no separate icon mark) - the paths * from the v1.0 brand guide's `simLogotype--dark.svg`, inlined so the logo - * ships as zero-request server-rendered HTML. + * ships as zero-request server-rendered HTML. They live in + * `@/lib/branding/wordmark` because the email header rasterizes the same + * outlines. * * Filled with a single solid `var(--text-body)` - the navbar's own text color * (the same token its nav-link chips use) - so the wordmark reads as one solid @@ -14,7 +18,7 @@ export function SimWordmark() { return ( - - - - + {WORDMARK_PATHS.map((d) => ( + + ))} ) diff --git a/apps/sim/components/emails/components/email-layout.tsx b/apps/sim/components/emails/components/email-layout.tsx index 253f7512055..8b7d3798940 100644 --- a/apps/sim/components/emails/components/email-layout.tsx +++ b/apps/sim/components/emails/components/email-layout.tsx @@ -1,18 +1,25 @@ import { Body, Container, Font, Head, Html, Img, Preview, Section } from '@react-email/components' import { baseStyles } from '@/components/emails/_styles' import { EmailFooter } from '@/components/emails/components/email-footer' +import { EMAIL_WORDMARK_SIZE } from '@/lib/branding/wordmark' import { getBaseUrl } from '@/lib/core/utils/urls' import { getBrandConfig } from '@/ee/whitelabeling' /** - * Wordmark display size — exactly 1/4 of `wordmark.png`'s intrinsic 272×164. - * The asset is a 4x source and must stay that way, since email clients do no - * responsive image selection; changing one dimension alone distorts the mark. + * Sim's own wordmark, rasterized from the shared brand outlines by + * `scripts/generate-email-wordmark.ts`. Both dimensions are pinned because + * email clients do no responsive image selection. */ -const WORDMARK_SIZE = { height: '41', width: '68' } as const +const WORDMARK_SIZE = { + width: String(EMAIL_WORDMARK_SIZE.width), + height: String(EMAIL_WORDMARK_SIZE.height), +} as const -/** Whitelabeled logos are arbitrary aspect ratios, so only height is pinned. */ -const CUSTOM_LOGO_SIZE = { height: '34' } as const +/** + * Whitelabeled logos are arbitrary aspect ratios, so only height is pinned — + * matched to the wordmark so both marks read at the same weight. + */ +const CUSTOM_LOGO_SIZE = { height: String(EMAIL_WORDMARK_SIZE.height) } as const interface EmailLayoutProps { /** Preview text shown in email client list view */ diff --git a/apps/sim/lib/branding/index.ts b/apps/sim/lib/branding/index.ts index 02cf0d799f1..cef1dbe4da4 100644 --- a/apps/sim/lib/branding/index.ts +++ b/apps/sim/lib/branding/index.ts @@ -1,3 +1,9 @@ export { defaultBrandConfig } from './defaults' export type { BrandConfig, ThemeColors } from './types' export { HEX_COLOR_REGEX } from './types' +export { + EMAIL_WORDMARK_SCALE, + EMAIL_WORDMARK_SIZE, + WORDMARK_PATHS, + WORDMARK_VIEW_BOX, +} from './wordmark' diff --git a/apps/sim/lib/branding/wordmark.test.ts b/apps/sim/lib/branding/wordmark.test.ts new file mode 100644 index 00000000000..fd1b157a803 --- /dev/null +++ b/apps/sim/lib/branding/wordmark.test.ts @@ -0,0 +1,49 @@ +/** + * @vitest-environment node + */ +import { readFileSync } from 'node:fs' +import path from 'node:path' +import { describe, expect, it } from 'vitest' +import { + EMAIL_WORDMARK_SCALE, + EMAIL_WORDMARK_SIZE, + WORDMARK_PATHS, + WORDMARK_VIEW_BOX, +} from '@/lib/branding/wordmark' + +const WORDMARK_PNG = path.join( + import.meta.dirname, + '..', + '..', + 'public', + 'brand', + 'color', + 'email', + 'wordmark.png' +) + +/** Reads width/height out of a PNG's IHDR, which always follows the 8-byte signature. */ +function readPngSize(file: string): { width: number; height: number } { + const buffer = readFileSync(file) + return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) } +} + +describe('email wordmark asset', () => { + it('is committed at the scale the email header renders it', () => { + expect(readPngSize(WORDMARK_PNG)).toEqual({ + width: EMAIL_WORDMARK_SIZE.width * EMAIL_WORDMARK_SCALE, + height: EMAIL_WORDMARK_SIZE.height * EMAIL_WORDMARK_SCALE, + }) + }) + + it('renders the mark at the brand outlines aspect ratio, within a rounding pixel', () => { + const boxAspect = EMAIL_WORDMARK_SIZE.width / EMAIL_WORDMARK_SIZE.height + const viewBoxAspect = WORDMARK_VIEW_BOX.width / WORDMARK_VIEW_BOX.height + expect(Math.abs(boxAspect - viewBoxAspect) / viewBoxAspect).toBeLessThan(0.01) + }) + + it('carries every glyph of the logotype', () => { + expect(WORDMARK_PATHS).toHaveLength(4) + for (const d of WORDMARK_PATHS) expect(d.startsWith('M')).toBe(true) + }) +}) diff --git a/apps/sim/lib/branding/wordmark.ts b/apps/sim/lib/branding/wordmark.ts new file mode 100644 index 00000000000..db8658b6d10 --- /dev/null +++ b/apps/sim/lib/branding/wordmark.ts @@ -0,0 +1,30 @@ +/** + * The "sim" logotype paths from the v1.0 brand guide (`simLogotype--dark.svg`). + * + * Shared so the two surfaces that draw the mark cannot drift. The landing + * navbar inlines them as server-rendered SVG; `scripts/generate-email-wordmark.ts` + * rasterizes the same paths into the PNG the email header uses, because email + * clients (Gmail chief among them) strip inline SVG. + */ +export const WORDMARK_VIEW_BOX = { width: 441, height: 212 } as const + +/** Glyph outlines, drawn in `WORDMARK_VIEW_BOX` coordinates. */ +export const WORDMARK_PATHS: readonly string[] = [ + 'M0 160.9H29.51C29.51 169.08 32.46 175.61 38.37 180.48C44.27 185.12 52.25 187.44 62.31 187.44C73.24 187.44 81.65 185.34 87.56 181.14C93.46 176.71 96.41 170.85 96.41 163.55C96.41 158.24 94.77 153.82 91.49 150.28C88.43 146.74 82.75 143.86 74.44 141.65L46.24 135.01C32.03 131.47 21.42 126.05 14.43 118.75C7.65 111.45 4.26 101.83 4.26 89.88C4.26 79.93 6.78 71.3 11.81 64C17.05 56.7 24.16 51.06 33.12 47.08C42.3 43.09 52.8 41.1 64.6 41.1C76.41 41.1 86.57 43.2 95.1 47.41C103.84 51.61 110.62 57.47 115.43 64.99C120.46 72.52 123.08 81.48 123.3 91.87H93.79C93.57 83.47 90.84 76.94 85.59 72.3C80.34 67.65 73.02 65.33 63.62 65.33C54 65.33 46.57 67.43 41.32 71.63C36.07 75.83 33.45 81.59 33.45 88.89C33.45 99.73 41.32 107.14 57.06 111.12L85.26 118.09C98.81 121.19 108.98 126.28 115.76 133.35C122.53 140.21 125.92 149.61 125.92 161.56C125.92 171.74 123.19 180.7 117.73 188.44C112.26 195.96 104.72 201.82 95.1 206.03C85.7 210.01 74.55 212 61.65 212C42.85 212 27.87 207.35 16.72 198.06C5.57 188.77 0 176.38 0 160.9Z', + 'M232.8 212H202.13L202.13 49.76H229.54V77.39C232.8 68.34 239.11 60.66 247.81 54.7C256.73 48.52 267.5 45.43 280.12 45.43C294.26 45.43 306.01 49.29 315.36 57.02C324.72 64.75 330.81 75.01 333.64 87.82H328.09C330.27 75.01 336.25 64.75 346.04 57.02C355.83 49.29 367.9 45.43 382.26 45.43C400.54 45.43 414.89 50.84 425.34 61.66C435.78 72.47 441 87.26 441 106.03V212H410.98V113.65C410.98 100.84 407.71 91.02 401.19 84.17C394.88 77.11 386.29 73.58 375.41 73.58C367.79 73.58 361.05 75.34 355.17 78.88C349.52 82.19 345.06 87.04 341.8 93.45C338.53 99.85 336.9 107.36 336.9 115.97V212H306.55V113.32C306.55 100.51 303.4 90.8 297.09 84.17C290.78 77.33 282.19 73.91 271.31 73.91C263.69 73.91 256.95 75.67 251.08 79.21C245.42 82.52 240.96 87.38 237.7 93.78C234.43 99.96 232.8 107.36 232.8 115.97V212Z', + 'M184.83 20.55C184.83 31.9 175.64 41.1 164.29 41.1C152.95 41.1 143.76 31.9 143.76 20.55C143.76 9.2 152.95 0 164.29 0C175.64 0 184.83 9.2 184.83 20.55Z', + 'M179.43 212H149.16V49.76C153.76 51.91 158.88 53.12 164.29 53.12C169.7 53.12 174.83 51.91 179.43 49.76V212Z', +] + +/** + * Display box for the email header wordmark. + * + * The landing navbar sizes the mark at its neighbouring text plus 2px above and + * below (18px ink against 14px chip labels). Email body copy is 16px, so the + * mark stands 20px tall; the width follows the view box. Both dimensions are + * pinned because email clients do no responsive image selection. + */ +export const EMAIL_WORDMARK_SIZE = { width: 42, height: 20 } as const + +/** Retina factor the email raster is generated at. */ +export const EMAIL_WORDMARK_SCALE = 4 diff --git a/apps/sim/public/brand/color/email/wordmark.png b/apps/sim/public/brand/color/email/wordmark.png index 2c3a6c314f2ce34a87bd1cc2ae91c0c996f536dd..6851edd5b39c04dfa4f736256cda64271356266b 100644 GIT binary patch literal 2538 zcmVWvam!?=p(&K;dXzlM)ql-w6QA#X-(LH1&i7b* zX3hDy=R0Tbb$;L8`>eJ0+Ch*-B|ks^nu3CY8SUG*-+<#@9RI-aE6B)p?b_7{q)3UP z6crVn4hY|b9I8kF>rMcCi)c$q6mYIi1ZRxgCt8ydT0ze^mjNgBr2ls4(4n1ZPKx&e z&{}ffJOa|MMSD`b7r^#{37`tnrlLV9z5!ih0qC8gK`FlHpW96Vg*+k}l;RuEpDh3# zDjJmH8_-1-fZiY)l;V31dXM4!b5CJm;VGg)DP9ZQm!|@_UkxA~FWQq5)-Zn^ z{FcBL%}EJ!sJ|89?biUbv}4DPbwy)Rf(Rm+aslAt3c&mmGEU~5LBcLOZVyLRn*azyje`lw59$Z*tU5TtX@JjnSDhxIdDo{PEaWi+QvCYbWn)U-6z!NYt| zJ!q33_j<@!)T@DOGacH|PyI;t7(UNmAd?}je41+N;07Oh1~XP@NycE@KF7fiYZGKX zuGbe2@e(M$$`SMCniT(uzl|mn6V7{B<9cN}ZJ;@LkT@K$Htso-Vo)9F>>tQ1KvmC1 zU-Ytw>x{CDWr!Qb=-fj`c0}C=;z{13sCY|2x5ZJ`z6$Q=9Z;Gk;r_#v^}xB#Xu70U z2ILs%s;gxTgHHVxp_6FjVo2|;ewBwJmKANFtZwty`Z~LC;ymGV05PjM9FX)DTp7v*itUsP*Ox z)f;&UuK79HJsl{!Te<ePDdS?l>3T|%6$%_t zb5>46@>3m9)Nz64dB4*VX%$ElYh=%%PCz*mCNQ(4#Jkr%awpS}s(JnvWq!)5$R155 zK-8}%&V5b3)g7jL3b+>@6mz0iXwy{GEEEJIK$?w6b)u1a*^6 z2ZZ_!C@wBOG2kW3s7=0l8}C@$7h(LNh;f1KaVfX=TD{Jw!}3%IG)euZRb=|n4cfsq zp73OrT40;)57#K9Ij>-1bqam%*4A9qn#po(Whz*>pOkK$u`WkLfr^jQ#c*^BNv;g!O<$X#jWJgUS zK+9ZNJu1*!zKtgb-)|duTnePD0h~H5wizZ$g7jLN5;>q;=YnB7SDU^Rg;2MVTuZ|- zlWk5gF@}R$0;o0syKG}xHQRZYA)svo|BDrCa7<5h?osA9WftHxY?c7Z^WFsh6ZSSU z0Q83-yd)Z{ICY~4u-GvF1I)$^7eG@d5t({f;<_Fuhxi<(FzePh_5w>yOcOQ+=WGC! zvuMvjTbFR^Mz-e=>(+-;H}WpVcEJM--vY@kHZWB|jTq2PLAbf77BB&A7Gm8xGr77l zh!a_~)h3)vZIsnLmD0#W4Cs=O+scYP4Ku?_US8hSR)8w^)DTB5Gn&sXRRPdNQTDD8 z16mStTPJdsDTiFQ#@2w+L|ZM@a2Y9a=7b_xT!JuLxMW~T;}5G>?OBDD1NZksjm0%(>18mLHV zAH%)Q0xB|J;dr?KDuC)Vx|mE!Q7lI#j}#fAX(NoYP{=&Qo56QO2Z?73ZQ}J zD$LtdKFF|p)bWN1u>{a;0W>%|IN>~sprMzW5L4yM;6f^ZW+|X?a{RP~jG#qNuGdS) zD0YMZDu4zKQiV4sA3YhXHJ7?-d#N}+WC2tF)!t!)*3+vT>t&X`=v)C*0QCuLB?>A* zB|bLwL?ul&C4dT`jw()>GJZ0EbhH2}fQC{!b?S5uH`5($gCc+mpkWUCjhN|D_Uhyc z6+i{hz+qs=IOcNRdIG2b8fGJ!Fo*dT0%!&UO6#@N4stq@7g8@;#^^yi3ZNMbDDAw) zG3_8<1FE*C<}9JD6@rqtGJ4Ql0W`CR_{yA=Bycg_=(7&SnY%X!--nNxi)vz34?A70lW7Y@rzHvj9*^vz zk(>BmQ@MS8%A@Ec3A`{*+UI?i=g%F&*`S)4*uUTp)Tz#VzNE83bku4fM%JillJbh-6c%D za1Eu6O35Teo*sHLr-gR3H@FH`F16f?eu^yl$U;Ds+cT0q%^9+Wn@(E*&7fcrT;yV( z3*Ibco9^C70%!(DGhrw$u-z2Y!%EIAoO!1Jn&IJgv`H58!TN-At&U4m0L`*^P}*g| zb_X-t+)T7XhHdg&0W^aHmMOeWt!>{Ca<93HrW%1{w-Z2hH+n^b#)L5JQ_iOzfukLF z1<)*8@ztEJ^%m!07*qoM6N<$g6U$X Am;e9( literal 10275 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 J3Kg61{{gq?$oBvM diff --git a/apps/sim/scripts/generate-email-wordmark.ts b/apps/sim/scripts/generate-email-wordmark.ts new file mode 100644 index 00000000000..2f4b2974f63 --- /dev/null +++ b/apps/sim/scripts/generate-email-wordmark.ts @@ -0,0 +1,59 @@ +/** + * Regenerates the email header wordmark raster from the shared brand outlines. + * + * Email clients strip inline SVG, so the header has to ship a PNG - but that PNG + * is derived from the same `WORDMARK_PATHS` the landing navbar renders, and + * filled with the same `textBody` ink the email's own body copy uses, so the two + * surfaces cannot drift apart. + * + * Run with `bun run apps/sim/scripts/generate-email-wordmark.ts` and commit the + * result; nothing regenerates it at build time. + */ + +import { writeFile } from 'node:fs/promises' +import path from 'node:path' +import { createLogger } from '@sim/logger' +import sharp from 'sharp' +import { colors } from '@/components/emails/_styles' +import { + EMAIL_WORDMARK_SCALE, + EMAIL_WORDMARK_SIZE, + WORDMARK_PATHS, + WORDMARK_VIEW_BOX, +} from '@/lib/branding/wordmark' + +const logger = createLogger('GenerateEmailWordmark') + +const OUTPUT_PATH = path.join( + import.meta.dirname, + '..', + 'public', + 'brand', + 'color', + 'email', + 'wordmark.png' +) + +function buildSvg(width: number, height: number, fill: string): string { + const paths = WORDMARK_PATHS.map((d) => ``).join('') + return `${paths}` +} + +async function main(): Promise { + const width = EMAIL_WORDMARK_SIZE.width * EMAIL_WORDMARK_SCALE + const height = EMAIL_WORDMARK_SIZE.height * EMAIL_WORDMARK_SCALE + const svg = buildSvg(width, height, colors.textBody) + + const png = await sharp(Buffer.from(svg)).png({ compressionLevel: 9 }).toBuffer() + await writeFile(OUTPUT_PATH, png) + + logger.info('Generated email wordmark', { + output: OUTPUT_PATH, + pixels: `${width}x${height}`, + displayedAt: `${EMAIL_WORDMARK_SIZE.width}x${EMAIL_WORDMARK_SIZE.height}`, + fill: colors.textBody, + bytes: png.byteLength, + }) +} + +await main() From e54b6ab48ec9dc8e6bdebfceb6dedb7d64bb17b7 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 12 Aug 2026 16:19:26 -0700 Subject: [PATCH 2/4] chore(emails): drop the wordmark generator script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The raster is committed at 4x, so the display box can be retuned without re-exporting it — the script only ever ran by hand, and the test pinning the asset to an exact multiple of the box would have forced a pointless regeneration on any size tweak. Keep the shared outlines the navbar and 8 other surfaces already render, record how the asset was produced on the size constant, and assert the property that actually matters: the asset out-resolves its display box. --- apps/sim/lib/branding/index.ts | 7 +-- apps/sim/lib/branding/wordmark.test.ts | 26 +++++---- apps/sim/lib/branding/wordmark.ts | 8 +-- apps/sim/scripts/generate-email-wordmark.ts | 59 --------------------- 4 files changed, 21 insertions(+), 79 deletions(-) delete mode 100644 apps/sim/scripts/generate-email-wordmark.ts diff --git a/apps/sim/lib/branding/index.ts b/apps/sim/lib/branding/index.ts index cef1dbe4da4..f21a7244d32 100644 --- a/apps/sim/lib/branding/index.ts +++ b/apps/sim/lib/branding/index.ts @@ -1,9 +1,4 @@ export { defaultBrandConfig } from './defaults' export type { BrandConfig, ThemeColors } from './types' export { HEX_COLOR_REGEX } from './types' -export { - EMAIL_WORDMARK_SCALE, - EMAIL_WORDMARK_SIZE, - WORDMARK_PATHS, - WORDMARK_VIEW_BOX, -} from './wordmark' +export { EMAIL_WORDMARK_SIZE, WORDMARK_PATHS, WORDMARK_VIEW_BOX } from './wordmark' diff --git a/apps/sim/lib/branding/wordmark.test.ts b/apps/sim/lib/branding/wordmark.test.ts index fd1b157a803..b73dd955804 100644 --- a/apps/sim/lib/branding/wordmark.test.ts +++ b/apps/sim/lib/branding/wordmark.test.ts @@ -4,12 +4,10 @@ import { readFileSync } from 'node:fs' import path from 'node:path' import { describe, expect, it } from 'vitest' -import { - EMAIL_WORDMARK_SCALE, - EMAIL_WORDMARK_SIZE, - WORDMARK_PATHS, - WORDMARK_VIEW_BOX, -} from '@/lib/branding/wordmark' +import { EMAIL_WORDMARK_SIZE, WORDMARK_PATHS, WORDMARK_VIEW_BOX } from '@/lib/branding/wordmark' + +/** Email clients do no responsive image selection, so the asset carries retina detail itself. */ +const MIN_RETINA_SCALE = 2 const WORDMARK_PNG = path.join( import.meta.dirname, @@ -29,11 +27,17 @@ function readPngSize(file: string): { width: number; height: number } { } describe('email wordmark asset', () => { - it('is committed at the scale the email header renders it', () => { - expect(readPngSize(WORDMARK_PNG)).toEqual({ - width: EMAIL_WORDMARK_SIZE.width * EMAIL_WORDMARK_SCALE, - height: EMAIL_WORDMARK_SIZE.height * EMAIL_WORDMARK_SCALE, - }) + it('out-resolves the box the email header renders it in', () => { + const { width, height } = readPngSize(WORDMARK_PNG) + expect(width).toBeGreaterThanOrEqual(EMAIL_WORDMARK_SIZE.width * MIN_RETINA_SCALE) + expect(height).toBeGreaterThanOrEqual(EMAIL_WORDMARK_SIZE.height * MIN_RETINA_SCALE) + }) + + it('is committed at the aspect ratio the header displays it at', () => { + const { width, height } = readPngSize(WORDMARK_PNG) + const assetAspect = width / height + const boxAspect = EMAIL_WORDMARK_SIZE.width / EMAIL_WORDMARK_SIZE.height + expect(Math.abs(assetAspect - boxAspect) / boxAspect).toBeLessThan(0.01) }) it('renders the mark at the brand outlines aspect ratio, within a rounding pixel', () => { diff --git a/apps/sim/lib/branding/wordmark.ts b/apps/sim/lib/branding/wordmark.ts index db8658b6d10..f7d2cc08cb5 100644 --- a/apps/sim/lib/branding/wordmark.ts +++ b/apps/sim/lib/branding/wordmark.ts @@ -23,8 +23,10 @@ export const WORDMARK_PATHS: readonly string[] = [ * below (18px ink against 14px chip labels). Email body copy is 16px, so the * mark stands 20px tall; the width follows the view box. Both dimensions are * pinned because email clients do no responsive image selection. + * + * The header renders `public/brand/color/email/wordmark.png` rather than these + * paths, because email clients strip inline SVG. That file is these outlines + * filled with the email palette's `textBody`, rasterized at 4x so the mark + * stays crisp on retina and this box can be retuned without re-exporting it. */ export const EMAIL_WORDMARK_SIZE = { width: 42, height: 20 } as const - -/** Retina factor the email raster is generated at. */ -export const EMAIL_WORDMARK_SCALE = 4 diff --git a/apps/sim/scripts/generate-email-wordmark.ts b/apps/sim/scripts/generate-email-wordmark.ts deleted file mode 100644 index 2f4b2974f63..00000000000 --- a/apps/sim/scripts/generate-email-wordmark.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Regenerates the email header wordmark raster from the shared brand outlines. - * - * Email clients strip inline SVG, so the header has to ship a PNG - but that PNG - * is derived from the same `WORDMARK_PATHS` the landing navbar renders, and - * filled with the same `textBody` ink the email's own body copy uses, so the two - * surfaces cannot drift apart. - * - * Run with `bun run apps/sim/scripts/generate-email-wordmark.ts` and commit the - * result; nothing regenerates it at build time. - */ - -import { writeFile } from 'node:fs/promises' -import path from 'node:path' -import { createLogger } from '@sim/logger' -import sharp from 'sharp' -import { colors } from '@/components/emails/_styles' -import { - EMAIL_WORDMARK_SCALE, - EMAIL_WORDMARK_SIZE, - WORDMARK_PATHS, - WORDMARK_VIEW_BOX, -} from '@/lib/branding/wordmark' - -const logger = createLogger('GenerateEmailWordmark') - -const OUTPUT_PATH = path.join( - import.meta.dirname, - '..', - 'public', - 'brand', - 'color', - 'email', - 'wordmark.png' -) - -function buildSvg(width: number, height: number, fill: string): string { - const paths = WORDMARK_PATHS.map((d) => ``).join('') - return `${paths}` -} - -async function main(): Promise { - const width = EMAIL_WORDMARK_SIZE.width * EMAIL_WORDMARK_SCALE - const height = EMAIL_WORDMARK_SIZE.height * EMAIL_WORDMARK_SCALE - const svg = buildSvg(width, height, colors.textBody) - - const png = await sharp(Buffer.from(svg)).png({ compressionLevel: 9 }).toBuffer() - await writeFile(OUTPUT_PATH, png) - - logger.info('Generated email wordmark', { - output: OUTPUT_PATH, - pixels: `${width}x${height}`, - displayedAt: `${EMAIL_WORDMARK_SIZE.width}x${EMAIL_WORDMARK_SIZE.height}`, - fill: colors.textBody, - bytes: png.byteLength, - }) -} - -await main() From ddc647c8fb1e99dfeeddcedebeeec96a1d287a46 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 12 Aug 2026 16:23:15 -0700 Subject: [PATCH 3/4] chore(branding): re-export the branding barrel through the absolute alias Matches the repo's absolute-import rule and the majority of lib barrels (billing, table, uploads and five others), instead of leaving this file split between relative and absolute re-exports. --- apps/sim/lib/branding/index.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/sim/lib/branding/index.ts b/apps/sim/lib/branding/index.ts index f21a7244d32..a99cc3a9227 100644 --- a/apps/sim/lib/branding/index.ts +++ b/apps/sim/lib/branding/index.ts @@ -1,4 +1,8 @@ -export { defaultBrandConfig } from './defaults' -export type { BrandConfig, ThemeColors } from './types' -export { HEX_COLOR_REGEX } from './types' -export { EMAIL_WORDMARK_SIZE, WORDMARK_PATHS, WORDMARK_VIEW_BOX } from './wordmark' +export { defaultBrandConfig } from '@/lib/branding/defaults' +export type { BrandConfig, ThemeColors } from '@/lib/branding/types' +export { HEX_COLOR_REGEX } from '@/lib/branding/types' +export { + EMAIL_WORDMARK_SIZE, + WORDMARK_PATHS, + WORDMARK_VIEW_BOX, +} from '@/lib/branding/wordmark' From d32b8607c27038c1b64f145d70603cb8d6a2cf27 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 12 Aug 2026 16:29:28 -0700 Subject: [PATCH 4/4] docs(branding): correct the wordmark comments that named the removed script Two TSDoc blocks still pointed at scripts/generate-email-wordmark.ts as how the email raster is produced. Point them at the committed export and the size constant that records its fill and scale instead. --- apps/sim/components/emails/components/email-layout.tsx | 6 +++--- apps/sim/lib/branding/wordmark.ts | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/sim/components/emails/components/email-layout.tsx b/apps/sim/components/emails/components/email-layout.tsx index 8b7d3798940..2b046586afc 100644 --- a/apps/sim/components/emails/components/email-layout.tsx +++ b/apps/sim/components/emails/components/email-layout.tsx @@ -6,9 +6,9 @@ import { getBaseUrl } from '@/lib/core/utils/urls' import { getBrandConfig } from '@/ee/whitelabeling' /** - * Sim's own wordmark, rasterized from the shared brand outlines by - * `scripts/generate-email-wordmark.ts`. Both dimensions are pinned because - * email clients do no responsive image selection. + * Sim's own wordmark: a committed raster export of the brand outlines in + * `@/lib/branding/wordmark`, since email clients strip inline SVG. Both + * dimensions are pinned because they do no responsive image selection either. */ const WORDMARK_SIZE = { width: String(EMAIL_WORDMARK_SIZE.width), diff --git a/apps/sim/lib/branding/wordmark.ts b/apps/sim/lib/branding/wordmark.ts index f7d2cc08cb5..580fbf17c55 100644 --- a/apps/sim/lib/branding/wordmark.ts +++ b/apps/sim/lib/branding/wordmark.ts @@ -1,10 +1,11 @@ /** * The "sim" logotype paths from the v1.0 brand guide (`simLogotype--dark.svg`). * - * Shared so the two surfaces that draw the mark cannot drift. The landing - * navbar inlines them as server-rendered SVG; `scripts/generate-email-wordmark.ts` - * rasterizes the same paths into the PNG the email header uses, because email - * clients (Gmail chief among them) strip inline SVG. + * Shared so the surfaces that draw the mark cannot drift: `SimWordmark` inlines + * them as server-rendered SVG for the navbar, footer, auth shell and settings + * sidebar. The email header cannot — clients (Gmail chief among them) strip + * inline SVG — so it renders a committed raster export of these same outlines; + * {@link EMAIL_WORDMARK_SIZE} records how that file is produced. */ export const WORDMARK_VIEW_BOX = { width: 441, height: 212 } as const