@mintlify/og 0.0.16 → 0.0.17

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 (39) hide show
  1. package/dist/constants.d.ts +4 -2
  2. package/dist/constants.js +3 -1
  3. package/dist/hash.js +11 -4
  4. package/dist/index.d.ts +3 -1
  5. package/dist/index.js +2 -0
  6. package/dist/inputs/default-font-urls.d.ts +2 -0
  7. package/dist/inputs/default-font-urls.js +2 -0
  8. package/dist/inputs/font-file.d.ts +1 -0
  9. package/dist/inputs/font-file.js +36 -0
  10. package/dist/inputs/fonts.d.ts +1 -0
  11. package/dist/inputs/fonts.js +18 -5
  12. package/dist/inputs/image.d.ts +12 -0
  13. package/dist/inputs/image.js +76 -6
  14. package/dist/inputs/oklch.d.ts +7 -0
  15. package/dist/inputs/oklch.js +45 -0
  16. package/dist/inputs/palette.d.ts +19 -0
  17. package/dist/inputs/palette.js +108 -0
  18. package/dist/inputs/resvg.d.ts +1 -0
  19. package/dist/inputs/resvg.js +12 -0
  20. package/dist/inputs/text-fit.d.ts +10 -0
  21. package/dist/inputs/text-fit.js +84 -0
  22. package/dist/inputs/theme.d.ts +4 -0
  23. package/dist/inputs/theme.js +2 -2
  24. package/dist/layouts/font-stack.d.ts +1 -0
  25. package/dist/layouts/font-stack.js +7 -0
  26. package/dist/layouts/index.d.ts +8 -3
  27. package/dist/layouts/index.js +8 -3
  28. package/dist/layouts/legacy.d.ts +2 -0
  29. package/dist/layouts/{classic.js → legacy.js} +3 -8
  30. package/dist/layouts/preset.d.ts +2 -0
  31. package/dist/layouts/preset.js +240 -0
  32. package/dist/layouts/types.d.ts +15 -1
  33. package/dist/render.js +40 -24
  34. package/dist/tsconfig.build.tsbuildinfo +1 -1
  35. package/dist/types.d.ts +10 -4
  36. package/fonts/Inter-Medium.ttf +0 -0
  37. package/fonts/PaperMono-Regular.ttf +0 -0
  38. package/package.json +5 -3
  39. package/dist/layouts/classic.d.ts +0 -2
@@ -1,7 +1,9 @@
1
- import type { OgLayout } from './types.js';
1
+ import type { OgPreset, OgSurface } from './types.js';
2
2
  export declare const OG_WIDTH = 1200;
3
3
  export declare const OG_HEIGHT = 630;
4
- export declare const DEFAULT_LAYOUT: OgLayout;
4
+ export declare const DEFAULT_PRESET: Extract<OgPreset, 'legacy'>;
5
+ export declare const DEFAULT_SURFACE: OgSurface;
5
6
  export declare const DEFAULT_FONT_FAMILY = "Inter";
7
+ export declare const MONO_FONT_FAMILY = "Paper Mono";
6
8
  export declare const LOGO_HEIGHT = 56;
7
9
  export declare const OG_RENDERER_REVISION = 1;
package/dist/constants.js CHANGED
@@ -1,7 +1,9 @@
1
1
  export const OG_WIDTH = 1200;
2
2
  export const OG_HEIGHT = 630;
3
- export const DEFAULT_LAYOUT = 'classic';
3
+ export const DEFAULT_PRESET = 'legacy';
4
+ export const DEFAULT_SURFACE = 'brand';
4
5
  export const DEFAULT_FONT_FAMILY = 'Inter';
6
+ export const MONO_FONT_FAMILY = 'Paper Mono';
5
7
  export const LOGO_HEIGHT = 56;
6
8
  // Bump whenever rendered output changes for the same input, so cards get new hashes.
7
9
  export const OG_RENDERER_REVISION = 1;
package/dist/hash.js CHANGED
@@ -10,9 +10,11 @@ var __rest = (this && this.__rest) || function (s, e) {
10
10
  return t;
11
11
  };
12
12
  import { createHash } from 'node:crypto';
13
- import { DEFAULT_LAYOUT, OG_RENDERER_REVISION } from './constants.js';
13
+ import { DEFAULT_PRESET, OG_RENDERER_REVISION } from './constants.js';
14
14
  import { buildOgCardContent } from './inputs/content.js';
15
+ import { resolveOgPalette } from './inputs/palette.js';
15
16
  import { resolveOgTheme } from './inputs/theme.js';
17
+ import { PRESET_EYEBROW_STYLES } from './layouts/index.js';
16
18
  const HASH_NAMESPACE = 'mintlify-og-card';
17
19
  const HASH_LENGTH = 32;
18
20
  const sha256 = (data) => {
@@ -30,14 +32,19 @@ const hashFont = (font) => {
30
32
  return [font.family, font.faces.map(({ weight, data }) => [weight, sha256(data)])];
31
33
  };
32
34
  export function getOgCardHash(input) {
33
- var _a;
34
- const _b = resolveOgTheme(input.theme), { background: _ } = _b, theme = __rest(_b, ["background"]);
35
+ var _a, _b;
36
+ const _c = resolveOgTheme(input.theme), { background: _ } = _c, theme = __rest(_c, ["background"]);
37
+ const preset = (_a = input.preset) !== null && _a !== void 0 ? _a : DEFAULT_PRESET;
38
+ const presetStyle = preset === DEFAULT_PRESET
39
+ ? null
40
+ : [(_b = input.eyebrowStyle) !== null && _b !== void 0 ? _b : PRESET_EYEBROW_STYLES[preset], resolveOgPalette(input.theme)];
35
41
  const hashInput = [
36
42
  HASH_NAMESPACE,
37
43
  OG_RENDERER_REVISION,
38
- (_a = input.layout) !== null && _a !== void 0 ? _a : DEFAULT_LAYOUT,
44
+ preset,
39
45
  buildOgCardContent(input),
40
46
  theme,
47
+ presetStyle,
41
48
  hashAsset(input.logo),
42
49
  hashAsset(input.background),
43
50
  hashFont(input.font),
package/dist/index.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  export { DEFAULT_FONT_FAMILY, OG_HEIGHT, OG_RENDERER_REVISION, OG_WIDTH } from './constants.js';
2
2
  export { buildOgCardContent, type OgCardContent } from './inputs/content.js';
3
+ export { getFontContentType } from './inputs/font-file.js';
3
4
  export { loadDefaultFont, loadGoogleFont } from './inputs/fonts.js';
4
5
  export { getOgCardHash } from './hash.js';
6
+ export { resolveOgLogoAppearance } from './inputs/palette.js';
5
7
  export type { OgLogo } from './inputs/image.js';
6
8
  export { renderOgCardPng, renderOgCardSvg } from './render.js';
7
9
  export { resolveOgAppearance, resolveOgTheme, type OgBackground, type OgTheme, } from './inputs/theme.js';
8
- export type { OgAppearance, OgCardInput, OgFont, OgFontFace, OgImageAsset, OgLayout, OgThemeInput, } from './types.js';
10
+ export type { OgAppearance, OgCardInput, OgFont, OgFontFace, OgImageAsset, OgEyebrowStyle, OgPreset, OgSurface, OgThemeInput, } from './types.js';
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  export { DEFAULT_FONT_FAMILY, OG_HEIGHT, OG_RENDERER_REVISION, OG_WIDTH } from './constants.js';
2
2
  export { buildOgCardContent } from './inputs/content.js';
3
+ export { getFontContentType } from './inputs/font-file.js';
3
4
  export { loadDefaultFont, loadGoogleFont } from './inputs/fonts.js';
4
5
  export { getOgCardHash } from './hash.js';
6
+ export { resolveOgLogoAppearance } from './inputs/palette.js';
5
7
  export { renderOgCardPng, renderOgCardSvg } from './render.js';
6
8
  export { resolveOgAppearance, resolveOgTheme, } from './inputs/theme.js';
@@ -1,5 +1,7 @@
1
1
  export declare const DEFAULT_FONT_URLS: {
2
2
  readonly 400: URL;
3
+ readonly 500: URL;
3
4
  readonly 600: URL;
4
5
  readonly 700: URL;
5
6
  };
7
+ export declare const MONO_FONT_URL: URL;
@@ -1,6 +1,8 @@
1
1
  // Static URL literals so bundlers and file tracing include the font files.
2
2
  export const DEFAULT_FONT_URLS = {
3
3
  400: new URL('../../fonts/Inter-Regular.ttf', import.meta.url),
4
+ 500: new URL('../../fonts/Inter-Medium.ttf', import.meta.url),
4
5
  600: new URL('../../fonts/Inter-SemiBold.ttf', import.meta.url),
5
6
  700: new URL('../../fonts/Inter-Bold.ttf', import.meta.url),
6
7
  };
8
+ export const MONO_FONT_URL = new URL('../../fonts/PaperMono-Regular.ttf', import.meta.url);
@@ -0,0 +1 @@
1
+ export declare const getFontContentType: (bytes: Uint8Array) => string | undefined;
@@ -0,0 +1,36 @@
1
+ const MAX_SFNT_BYTES = 10 * 1024 * 1024;
2
+ const WOFF_HEADER_BYTES = 44;
3
+ const WOFF_TABLE_ENTRY_BYTES = 20;
4
+ const FONT_SIGNATURES = [
5
+ [[0x77, 0x4f, 0x46, 0x46], 'font/woff'],
6
+ [[0x00, 0x01, 0x00, 0x00], 'font/ttf'],
7
+ [[0x74, 0x72, 0x75, 0x65], 'font/ttf'],
8
+ [[0x4f, 0x54, 0x54, 0x4f], 'font/otf'],
9
+ ];
10
+ const hasSafeWoffTables = (bytes) => {
11
+ if (bytes.byteLength < WOFF_HEADER_BYTES)
12
+ return false;
13
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
14
+ const directoryEnd = WOFF_HEADER_BYTES + view.getUint16(12) * WOFF_TABLE_ENTRY_BYTES;
15
+ if (view.getUint32(8) !== bytes.byteLength || directoryEnd > bytes.byteLength)
16
+ return false;
17
+ if (view.getUint32(16) > MAX_SFNT_BYTES)
18
+ return false;
19
+ let totalTableBytes = 0;
20
+ for (let entry = WOFF_HEADER_BYTES; entry < directoryEnd; entry += WOFF_TABLE_ENTRY_BYTES) {
21
+ const offset = view.getUint32(entry + 4);
22
+ const compressedLength = view.getUint32(entry + 8);
23
+ const length = view.getUint32(entry + 12);
24
+ if (offset + compressedLength > bytes.byteLength || compressedLength > length)
25
+ return false;
26
+ totalTableBytes += length;
27
+ }
28
+ return totalTableBytes <= MAX_SFNT_BYTES;
29
+ };
30
+ export const getFontContentType = (bytes) => {
31
+ var _a;
32
+ const contentType = (_a = FONT_SIGNATURES.find(([signature]) => signature.every((byte, index) => bytes[index] === byte))) === null || _a === void 0 ? void 0 : _a[1];
33
+ if (contentType === 'font/woff' && !hasSafeWoffTables(bytes))
34
+ return undefined;
35
+ return contentType;
36
+ };
@@ -1,3 +1,4 @@
1
1
  import type { OgFont } from '../types.js';
2
2
  export declare const loadDefaultFont: () => Promise<OgFont>;
3
+ export declare const loadMonoFont: () => Promise<OgFont>;
3
4
  export declare const loadGoogleFont: (family: string, text: string) => Promise<OgFont | null>;
@@ -8,14 +8,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { readFile } from 'node:fs/promises';
11
- import { DEFAULT_FONT_FAMILY } from '../constants.js';
12
- import { DEFAULT_FONT_URLS } from './default-font-urls.js';
13
- const FONT_WEIGHTS = [400, 600, 700];
11
+ import { DEFAULT_FONT_FAMILY, MONO_FONT_FAMILY } from '../constants.js';
12
+ import { DEFAULT_FONT_URLS, MONO_FONT_URL } from './default-font-urls.js';
13
+ const FONT_WEIGHTS = [400, 500, 600, 700];
14
+ const REQUIRED_GOOGLE_FONT_WEIGHTS = [400, 600, 700];
15
+ const OPTIONAL_GOOGLE_FONT_WEIGHT = 500;
14
16
  const GOOGLE_FONTS_CSS_URL = 'https://fonts.googleapis.com/css2';
15
17
  // Google Fonts picks the format from the User-Agent; browser agents get WOFF2, which Satori can't read.
16
18
  const GOOGLE_FONTS_USER_AGENT = 'MintlifyOG/1.0';
17
19
  const SATORI_FONT_SOURCE = /src: url\((.+?)\) format\('(opentype|truetype|woff)'\)/;
18
20
  let defaultFontPromise;
21
+ let monoFontPromise;
19
22
  const readDefaultFontFace = (weight) => __awaiter(void 0, void 0, void 0, function* () {
20
23
  return ({
21
24
  weight,
@@ -29,6 +32,13 @@ export const loadDefaultFont = () => {
29
32
  })));
30
33
  return defaultFontPromise;
31
34
  };
35
+ export const loadMonoFont = () => {
36
+ monoFontPromise !== null && monoFontPromise !== void 0 ? monoFontPromise : (monoFontPromise = readFile(MONO_FONT_URL).then((data) => ({
37
+ family: MONO_FONT_FAMILY,
38
+ faces: [{ weight: 400, data }],
39
+ })));
40
+ return monoFontPromise;
41
+ };
32
42
  const getGoogleFontCssUrl = (family, text, weight) => {
33
43
  const encodedFamily = encodeURIComponent(family).replace(/%20/g, '+');
34
44
  return `${GOOGLE_FONTS_CSS_URL}?family=${encodedFamily}:wght@${weight}&text=${encodeURIComponent(text)}`;
@@ -48,8 +58,11 @@ const fetchGoogleFontFace = (family, text, weight) => __awaiter(void 0, void 0,
48
58
  });
49
59
  export const loadGoogleFont = (family, text) => __awaiter(void 0, void 0, void 0, function* () {
50
60
  try {
51
- const faces = yield Promise.all(FONT_WEIGHTS.map((weight) => fetchGoogleFontFace(family, text, weight)));
52
- return { family, faces };
61
+ const [faces, optionalFace] = yield Promise.all([
62
+ Promise.all(REQUIRED_GOOGLE_FONT_WEIGHTS.map((weight) => fetchGoogleFontFace(family, text, weight))),
63
+ fetchGoogleFontFace(family, text, OPTIONAL_GOOGLE_FONT_WEIGHT).catch(() => null),
64
+ ]);
65
+ return { family, faces: optionalFace ? [...faces, optionalFace] : faces };
53
66
  }
54
67
  catch (error) {
55
68
  console.error(`Failed to load Google Font "${family}":`, error);
@@ -4,5 +4,17 @@ export type OgLogo = {
4
4
  src: string;
5
5
  style: CSSProperties;
6
6
  };
7
+ export type OgPresetLogo = {
8
+ src: string;
9
+ width: number;
10
+ height: number;
11
+ image: {
12
+ left: number;
13
+ top: number;
14
+ width: number;
15
+ height: number;
16
+ };
17
+ };
7
18
  export declare const toDataUri: ({ bytes, contentType }: OgImageAsset) => string;
8
19
  export declare const buildOgLogo: (asset: OgImageAsset) => OgLogo;
20
+ export declare const buildPresetLogo: (asset: OgImageAsset) => OgPresetLogo;
@@ -1,13 +1,14 @@
1
+ import { Resvg } from '@resvg/resvg-wasm';
1
2
  import { imageSize } from 'image-size';
2
3
  import { LOGO_HEIGHT } from '../constants.js';
3
4
  const SVG_CONTENT_TYPE = 'image/svg+xml';
5
+ const PRESET_LOGO_MAX_WIDTH = 320;
6
+ const SVG_SCAN_SIZE = 640;
4
7
  export const toDataUri = ({ bytes, contentType }) => `data:${contentType};base64,${Buffer.from(bytes).toString('base64')}`;
5
- const getAspectRatio = (bytes) => {
8
+ const getSize = (bytes) => {
6
9
  try {
7
10
  const { width, height } = imageSize(bytes);
8
- if (!width || !height)
9
- return null;
10
- return width / height;
11
+ return width && height ? { width, height } : null;
11
12
  }
12
13
  catch (_a) {
13
14
  return null;
@@ -16,9 +17,10 @@ const getAspectRatio = (bytes) => {
16
17
  const getLogoStyle = (asset) => {
17
18
  if (asset.contentType !== SVG_CONTENT_TYPE)
18
19
  return { objectFit: 'contain' };
19
- const aspectRatio = getAspectRatio(asset.bytes);
20
- if (!aspectRatio)
20
+ const size = getSize(asset.bytes);
21
+ if (!size)
21
22
  return { objectFit: 'contain' };
23
+ const aspectRatio = size.width / size.height;
22
24
  return {
23
25
  height: `${LOGO_HEIGHT}px`,
24
26
  width: `${aspectRatio * LOGO_HEIGHT}px`,
@@ -29,3 +31,71 @@ export const buildOgLogo = (asset) => ({
29
31
  src: toDataUri(asset),
30
32
  style: getLogoStyle(asset),
31
33
  });
34
+ const getOpaqueBounds = (pixels, width, height) => {
35
+ let [left, top, right, bottom] = [width, height, -1, -1];
36
+ for (let y = 0; y < height; y++) {
37
+ for (let x = 0; x < width; x++) {
38
+ if (pixels[(y * width + x) * 4 + 3] === 0)
39
+ continue;
40
+ left = Math.min(left, x);
41
+ right = Math.max(right, x);
42
+ top = Math.min(top, y);
43
+ bottom = Math.max(bottom, y);
44
+ }
45
+ }
46
+ if (right < 0)
47
+ return null;
48
+ return { x: left, y: top, width: right - left + 1, height: bottom - top + 1 };
49
+ };
50
+ const getSvgBoxes = (bytes) => {
51
+ try {
52
+ const size = new Resvg(bytes);
53
+ const canvas = { x: 0, y: 0, width: size.width, height: size.height };
54
+ size.free();
55
+ const zoom = SVG_SCAN_SIZE / Math.max(canvas.width, canvas.height);
56
+ const resvg = new Resvg(bytes, { fitTo: { mode: 'zoom', value: zoom } });
57
+ const image = resvg.render();
58
+ const bounds = getOpaqueBounds(image.pixels, image.width, image.height);
59
+ const scale = canvas.width / image.width;
60
+ image.free();
61
+ resvg.free();
62
+ if (!bounds)
63
+ return { canvas, content: canvas };
64
+ return {
65
+ canvas,
66
+ content: {
67
+ x: bounds.x * scale,
68
+ y: bounds.y * scale,
69
+ width: bounds.width * scale,
70
+ height: bounds.height * scale,
71
+ },
72
+ };
73
+ }
74
+ catch (_a) {
75
+ return null;
76
+ }
77
+ };
78
+ const getLogoBoxes = (asset) => {
79
+ var _a;
80
+ const svg = asset.contentType === SVG_CONTENT_TYPE ? getSvgBoxes(asset.bytes) : null;
81
+ if (svg)
82
+ return svg;
83
+ const size = (_a = getSize(asset.bytes)) !== null && _a !== void 0 ? _a : { width: 1, height: 1 };
84
+ const canvas = Object.assign({ x: 0, y: 0 }, size);
85
+ return { canvas, content: canvas };
86
+ };
87
+ export const buildPresetLogo = (asset) => {
88
+ const { canvas, content } = getLogoBoxes(asset);
89
+ const scale = Math.min(LOGO_HEIGHT / content.height, PRESET_LOGO_MAX_WIDTH / content.width);
90
+ return {
91
+ src: toDataUri(asset),
92
+ width: content.width * scale,
93
+ height: content.height * scale,
94
+ image: {
95
+ left: -content.x * scale,
96
+ top: -content.y * scale,
97
+ width: canvas.width * scale,
98
+ height: canvas.height * scale,
99
+ },
100
+ };
101
+ };
@@ -0,0 +1,7 @@
1
+ export type Oklch = {
2
+ l: number;
3
+ c: number;
4
+ h: number;
5
+ };
6
+ export declare const toOklch: (hex: string) => Oklch;
7
+ export declare const oklchToHex: (color: Oklch) => string;
@@ -0,0 +1,45 @@
1
+ import { hexRgb, rgbToHex } from '@mintlify/common/colors';
2
+ const CHROMA_STEP = 0.002;
3
+ const GAMUT_EPSILON = 1e-4;
4
+ const toLinear = (channel) => {
5
+ const value = channel / 255;
6
+ return value <= 0.04045 ? value / 12.92 : Math.pow(((value + 0.055) / 1.055), 2.4);
7
+ };
8
+ const fromLinear = (value) => {
9
+ const clamped = Math.min(1, Math.max(0, value));
10
+ const encoded = clamped <= 0.0031308 ? clamped * 12.92 : 1.055 * Math.pow(clamped, (1 / 2.4)) - 0.055;
11
+ return Math.round(encoded * 255);
12
+ };
13
+ export const toOklch = (hex) => {
14
+ const { red, green, blue } = hexRgb(hex);
15
+ const [r, g, b] = [toLinear(red), toLinear(green), toLinear(blue)];
16
+ const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
17
+ const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
18
+ const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
19
+ const lightness = 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s;
20
+ const a = 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s;
21
+ const bAxis = 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s;
22
+ const hue = (Math.atan2(bAxis, a) * 180) / Math.PI;
23
+ return { l: lightness, c: Math.hypot(a, bAxis), h: (hue + 360) % 360 };
24
+ };
25
+ const toLinearRgb = ({ l, c, h }) => {
26
+ const radians = (h * Math.PI) / 180;
27
+ const a = c * Math.cos(radians);
28
+ const b = c * Math.sin(radians);
29
+ const lCube = Math.pow((l + 0.3963377774 * a + 0.2158037573 * b), 3);
30
+ const mCube = Math.pow((l - 0.1055613458 * a - 0.0638541728 * b), 3);
31
+ const sCube = Math.pow((l - 0.0894841775 * a - 1.291485548 * b), 3);
32
+ return [
33
+ 4.0767416621 * lCube - 3.3077115913 * mCube + 0.2309699292 * sCube,
34
+ -1.2684380046 * lCube + 2.6097574011 * mCube - 0.3413193965 * sCube,
35
+ -0.0041960863 * lCube - 0.7034186147 * mCube + 1.707614701 * sCube,
36
+ ];
37
+ };
38
+ const isInGamut = (color) => toLinearRgb(color).every((value) => value >= -GAMUT_EPSILON && value <= 1 + GAMUT_EPSILON);
39
+ export const oklchToHex = (color) => {
40
+ let chroma = color.c;
41
+ while (chroma > 0 && !isInGamut(Object.assign(Object.assign({}, color), { c: chroma })))
42
+ chroma -= CHROMA_STEP;
43
+ const [red, green, blue] = toLinearRgb(Object.assign(Object.assign({}, color), { c: Math.max(0, chroma) }));
44
+ return rgbToHex({ r: fromLinear(red), g: fromLinear(green), b: fromLinear(blue), a: 1 });
45
+ };
@@ -0,0 +1,19 @@
1
+ import type { OgAppearance, OgPreset, OgThemeInput } from '../types.js';
2
+ export type OgPalette = {
3
+ background: string;
4
+ line: string;
5
+ panel: string;
6
+ panelBorder: string | null;
7
+ glow: {
8
+ color: string;
9
+ appearance: OgAppearance;
10
+ } | null;
11
+ title: string;
12
+ description: string;
13
+ eyebrow: string;
14
+ badge: string;
15
+ badgeText: string;
16
+ };
17
+ export declare const isBrandSurface: (theme: OgThemeInput) => boolean;
18
+ export declare const resolveOgPalette: (theme: OgThemeInput) => OgPalette;
19
+ export declare const resolveOgLogoAppearance: (theme: OgThemeInput, preset: OgPreset) => OgAppearance;
@@ -0,0 +1,108 @@
1
+ import { combineColors, getDefaultBackgroundColors, hexRgb, rgbToHex, } from '@mintlify/common/colors';
2
+ import { DEFAULT_SURFACE } from '../constants.js';
3
+ import { oklchToHex, toOklch } from './oklch.js';
4
+ import { resolveBrandColors, resolveOgAppearance } from './theme.js';
5
+ const INK = '#1A1A1A';
6
+ const WHITE = '#FFFFFF';
7
+ const NEUTRAL_BACKGROUNDS = { light: '#FFFFFF', dark: '#111111' };
8
+ const DESCRIPTION_FADE = 0.22;
9
+ const LINE_WEIGHT_ON_LIGHT = 0.2;
10
+ const LINE_WEIGHT_ON_DARK = 0.15;
11
+ const NEUTRAL_PANEL_WEIGHT = 0.06;
12
+ const BRAND_DARK_SURFACE = { lightness: 0.17, maxChroma: 0.03 };
13
+ const BRAND_STEPS = {
14
+ light: {
15
+ line: { lightnessStep: -0.14, maxChroma: 0.03 },
16
+ panelBorder: { lightnessStep: -0.1, maxChroma: 0.04 },
17
+ },
18
+ dark: {
19
+ line: { lightnessStep: 0.12, maxChroma: 0.03 },
20
+ panelBorder: { lightnessStep: 0.08, maxChroma: 0.03 },
21
+ },
22
+ };
23
+ const GLOW_TONE = { lightness: 0.58, maxChroma: 0.16 };
24
+ const MIN_GLOW_SOURCE_CHROMA = 0.03;
25
+ const BRAND_PANELS = {
26
+ light: 'rgba(255, 255, 255, 0.7)',
27
+ dark: 'rgba(0, 0, 0, 0.35)',
28
+ };
29
+ const MIN_TEXT_CONTRAST = 3;
30
+ const MIN_EYEBROW_CONTRAST = 2;
31
+ const mix = (base, overlay, weight) => rgbToHex(combineColors(base, 1, overlay, weight));
32
+ const toLinear = (channel) => {
33
+ const value = channel / 255;
34
+ return value <= 0.03928 ? value / 12.92 : Math.pow(((value + 0.055) / 1.055), 2.4);
35
+ };
36
+ const luminance = (hex) => {
37
+ const { red, green, blue } = hexRgb(hex);
38
+ return 0.2126 * toLinear(red) + 0.7152 * toLinear(green) + 0.0722 * toLinear(blue);
39
+ };
40
+ const contrast = (a, b) => {
41
+ const [first, second] = [luminance(a), luminance(b)];
42
+ return (Math.max(first, second) + 0.05) / (Math.min(first, second) + 0.05);
43
+ };
44
+ export const isBrandSurface = (theme) => { var _a; return ((_a = theme.surface) !== null && _a !== void 0 ? _a : DEFAULT_SURFACE) === 'brand'; };
45
+ const toneOf = (hex, { lightness, maxChroma }) => {
46
+ const { c, h } = toOklch(hex);
47
+ return oklchToHex({ l: lightness, c: Math.min(c, maxChroma), h });
48
+ };
49
+ const stepFrom = (surface, hex, { lightnessStep, maxChroma }) => toneOf(hex, { lightness: toOklch(surface).l + lightnessStep, maxChroma });
50
+ const glowSource = (theme) => {
51
+ const { primaryColor, lightColor } = resolveBrandColors(theme);
52
+ return toOklch(primaryColor).c >= MIN_GLOW_SOURCE_CHROMA ? primaryColor : lightColor;
53
+ };
54
+ const textOn = (background) => contrast(WHITE, background) >= MIN_TEXT_CONTRAST ? WHITE : INK;
55
+ const getCustomBackground = (theme, appearance) => {
56
+ const background = appearance === 'light' ? theme.backgroundLight : theme.backgroundDark;
57
+ const generated = getDefaultBackgroundColors(theme.lightColor)[appearance];
58
+ return background && background.toLowerCase() !== generated.toLowerCase()
59
+ ? background
60
+ : undefined;
61
+ };
62
+ const resolveBackground = (theme, appearance, primary) => {
63
+ if (isBrandSurface(theme)) {
64
+ const custom = getCustomBackground(theme, appearance);
65
+ if (custom)
66
+ return custom;
67
+ return appearance === 'light' ? WHITE : toneOf(primary, BRAND_DARK_SURFACE);
68
+ }
69
+ const themeBackground = appearance === 'light' ? theme.backgroundLight : theme.backgroundDark;
70
+ return themeBackground || NEUTRAL_BACKGROUNDS[appearance];
71
+ };
72
+ const resolveEyebrowColor = (theme, appearance, background, title) => {
73
+ if (theme.eyebrowColor)
74
+ return theme.eyebrowColor;
75
+ const { primaryColor, lightColor } = resolveBrandColors(theme);
76
+ const accent = appearance === 'light' ? primaryColor : lightColor;
77
+ return contrast(accent, background) >= MIN_EYEBROW_CONTRAST ? accent : title;
78
+ };
79
+ const resolveSurfaceDetails = (theme, appearance, background, title) => {
80
+ if (!isBrandSurface(theme)) {
81
+ return {
82
+ line: mix(background, title, title === WHITE ? LINE_WEIGHT_ON_DARK : LINE_WEIGHT_ON_LIGHT),
83
+ panel: mix(background, title, NEUTRAL_PANEL_WEIGHT),
84
+ panelBorder: null,
85
+ glow: null,
86
+ };
87
+ }
88
+ const { primaryColor } = resolveBrandColors(theme);
89
+ const steps = BRAND_STEPS[appearance];
90
+ return {
91
+ line: stepFrom(background, primaryColor, steps.line),
92
+ panel: BRAND_PANELS[appearance],
93
+ panelBorder: stepFrom(background, primaryColor, steps.panelBorder),
94
+ glow: { color: toneOf(glowSource(theme), GLOW_TONE), appearance },
95
+ };
96
+ };
97
+ export const resolveOgPalette = (theme) => {
98
+ const appearance = resolveOgAppearance(theme);
99
+ const background = resolveBackground(theme, appearance, resolveBrandColors(theme).primaryColor);
100
+ const title = textOn(background);
101
+ const eyebrow = resolveEyebrowColor(theme, appearance, background, title);
102
+ return Object.assign(Object.assign({ background }, resolveSurfaceDetails(theme, appearance, background, title)), { title, description: mix(title, background, DESCRIPTION_FADE), eyebrow, badge: eyebrow, badgeText: textOn(eyebrow) });
103
+ };
104
+ export const resolveOgLogoAppearance = (theme, preset) => {
105
+ if (preset === 'legacy')
106
+ return resolveOgAppearance(theme);
107
+ return resolveOgPalette(theme).title === WHITE ? 'dark' : 'light';
108
+ };
@@ -0,0 +1 @@
1
+ export declare const initResvg: () => Promise<void>;
@@ -0,0 +1,12 @@
1
+ import { initWasm } from '@resvg/resvg-wasm';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { createRequire } from 'node:module';
4
+ const require = createRequire(import.meta.url);
5
+ let resvgReady;
6
+ export const initResvg = () => {
7
+ resvgReady !== null && resvgReady !== void 0 ? resvgReady : (resvgReady = initWasm(readFile(require.resolve('@resvg/resvg-wasm/index_bg.wasm'))).catch((error) => {
8
+ resvgReady = undefined;
9
+ throw error;
10
+ }));
11
+ return resvgReady;
12
+ };
@@ -0,0 +1,10 @@
1
+ import type { OgFont, OgFontFace } from '../types.js';
2
+ export type OgTextBox = {
3
+ fontSize: number;
4
+ fontWeight: OgFontFace['weight'];
5
+ letterSpacing: number;
6
+ width: number;
7
+ maxLines: number;
8
+ };
9
+ export type OgTextFitter = (text: string, box: OgTextBox) => string[];
10
+ export declare const createTextFitter: (fonts: OgFont[]) => OgTextFitter;
@@ -0,0 +1,84 @@
1
+ import opentype from '@shuding/opentype.js';
2
+ const ELLIPSIS = '…';
3
+ const BREAK_AFTER = /(?<=[\s/-])/;
4
+ const parsedFonts = new WeakMap();
5
+ const toArrayBuffer = (data) => {
6
+ if (data instanceof ArrayBuffer)
7
+ return data;
8
+ const copy = new ArrayBuffer(data.byteLength);
9
+ new Uint8Array(copy).set(data);
10
+ return copy;
11
+ };
12
+ const parseFace = (face) => {
13
+ const cached = parsedFonts.get(face.data);
14
+ if (cached)
15
+ return cached;
16
+ const font = opentype.parse(toArrayBuffer(face.data));
17
+ parsedFonts.set(face.data, font);
18
+ return font;
19
+ };
20
+ const closestFace = (font, weight) => font.faces.reduce((best, face) => Math.abs(face.weight - weight) < Math.abs(best.weight - weight) ? face : best);
21
+ const createMeasure = (fonts, { fontSize, letterSpacing }) => {
22
+ const options = { letterSpacing: letterSpacing / fontSize };
23
+ const fontFor = (char) => { var _a; return (_a = fonts.find((font) => font.charToGlyphIndex(char) > 0)) !== null && _a !== void 0 ? _a : fonts[0]; };
24
+ return (text) => {
25
+ const runs = [];
26
+ for (const char of text) {
27
+ const font = fontFor(char);
28
+ const last = runs.at(-1);
29
+ if (last && last.font === font)
30
+ last.text += char;
31
+ else
32
+ runs.push({ font, text: char });
33
+ }
34
+ return runs.reduce((width, run) => { var _a, _b; return width + ((_b = (_a = run.font) === null || _a === void 0 ? void 0 : _a.getAdvanceWidth(run.text, fontSize, options)) !== null && _b !== void 0 ? _b : 0); }, 0);
35
+ };
36
+ };
37
+ const splitToFit = (token, fits) => {
38
+ const chars = Array.from(token);
39
+ let length = 1;
40
+ while (length < chars.length && fits(chars.slice(0, length + 1).join('')))
41
+ length++;
42
+ return [chars.slice(0, length).join(''), chars.slice(length).join('')];
43
+ };
44
+ const wrap = (text, fits, maxLines) => {
45
+ const lines = [];
46
+ let current = '';
47
+ for (let token of text.split(BREAK_AFTER)) {
48
+ if (fits((current + token).trimEnd())) {
49
+ current += token;
50
+ continue;
51
+ }
52
+ if (current)
53
+ lines.push(current.trimEnd());
54
+ while (!fits(token.trimEnd()) && lines.length <= maxLines) {
55
+ const [head, rest] = splitToFit(token, fits);
56
+ lines.push(head);
57
+ token = rest;
58
+ }
59
+ current = token;
60
+ if (lines.length > maxLines)
61
+ return lines;
62
+ }
63
+ if (current.trim())
64
+ lines.push(current.trimEnd());
65
+ return lines;
66
+ };
67
+ const ellipsize = (line, fits) => {
68
+ const chars = Array.from(line);
69
+ while (chars.length > 0 && !fits(`${chars.join('').trimEnd()}${ELLIPSIS}`))
70
+ chars.pop();
71
+ return `${chars.join('').trimEnd()}${ELLIPSIS}`;
72
+ };
73
+ export const createTextFitter = (fonts) => (text, box) => {
74
+ var _a;
75
+ const faces = fonts.map((font) => parseFace(closestFace(font, box.fontWeight)));
76
+ const measure = createMeasure(faces, box);
77
+ const fits = (line) => measure(line) <= box.width;
78
+ const lines = wrap(text.replace(/\s+/g, ' ').trim(), fits, box.maxLines);
79
+ if (lines.length <= box.maxLines)
80
+ return lines;
81
+ const kept = lines.slice(0, box.maxLines);
82
+ const last = (_a = kept.pop()) !== null && _a !== void 0 ? _a : '';
83
+ return [...kept, ellipsize(last, fits)];
84
+ };
@@ -13,5 +13,9 @@ export type OgTheme = {
13
13
  backgroundColor: string;
14
14
  background: OgBackground;
15
15
  };
16
+ type BrandColors = Pick<OgTheme, 'primaryColor' | 'lightColor'>;
17
+ export declare const toRgba: (hex: string, alpha: number) => string;
16
18
  export declare const resolveOgAppearance: (theme: OgThemeInput) => OgAppearance;
19
+ export declare const resolveBrandColors: ({ primaryColor, lightColor }: OgThemeInput) => BrandColors;
17
20
  export declare const resolveOgTheme: (theme: OgThemeInput, backgroundImageUrl?: string) => OgTheme;
21
+ export {};
@@ -17,7 +17,7 @@ const GRADIENT_FADE_ALPHA = 0.004;
17
17
  const GRADIENT_FADE_STOP = '85%';
18
18
  const GRADIENT_CENTER = '1200px 800px';
19
19
  const tint = (baseColor, color) => rgbToHex(combineColors(baseColor, TINT_BASE_WEIGHT, color, TINT_COLOR_WEIGHT));
20
- const toRgba = (hex, alpha) => {
20
+ export const toRgba = (hex, alpha) => {
21
21
  const { red, green, blue } = hexRgb(hex);
22
22
  return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
23
23
  };
@@ -35,7 +35,7 @@ export const resolveOgAppearance = (theme) => {
35
35
  return 'light';
36
36
  return 'dark';
37
37
  };
38
- const resolveBrandColors = ({ primaryColor, lightColor }) => {
38
+ export const resolveBrandColors = ({ primaryColor, lightColor }) => {
39
39
  if (primaryColor && lightColor)
40
40
  return { primaryColor, lightColor };
41
41
  if (primaryColor)
@@ -0,0 +1 @@
1
+ export declare const getFontStack: (fontFamily: string) => string;
@@ -0,0 +1,7 @@
1
+ import { DEFAULT_FONT_FAMILY } from '../constants.js';
2
+ const FALLBACK_FONT_STACK = 'Inter var, sans-serif';
3
+ export const getFontStack = (fontFamily) => {
4
+ if (fontFamily === DEFAULT_FONT_FAMILY)
5
+ return FALLBACK_FONT_STACK;
6
+ return `${fontFamily}, ${FALLBACK_FONT_STACK}`;
7
+ };