diff --git a/.changeset/breezy-seals-yell.md b/.changeset/breezy-seals-yell.md new file mode 100644 index 0000000000..79235382af --- /dev/null +++ b/.changeset/breezy-seals-yell.md @@ -0,0 +1,5 @@ +--- +'@gitbook/colors': minor +--- + +Initial release diff --git a/bun.lock b/bun.lock index d0591adc22..0f48fcf4b8 100644 --- a/bun.lock +++ b/bun.lock @@ -22,6 +22,13 @@ "wrangler": "3.82.0", }, }, + "packages/colors": { + "name": "@gitbook/colors", + "version": "0.1.0", + "devDependencies": { + "typescript": "^5.5.3", + }, + }, "packages/emoji-codepoints": { "name": "@gitbook/emoji-codepoints", "version": "0.2.0", @@ -35,6 +42,7 @@ "dependencies": { "@gitbook/api": "^0.93.0", "@gitbook/cache-do": "workspace:*", + "@gitbook/colors": "workspace:*", "@gitbook/emoji-codepoints": "workspace:*", "@gitbook/icons": "workspace:*", "@gitbook/openapi-parser": "workspace:*", @@ -594,6 +602,8 @@ "@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"], + "@gitbook/colors": ["@gitbook/colors@workspace:packages/colors"], + "@gitbook/emoji-codepoints": ["@gitbook/emoji-codepoints@workspace:packages/emoji-codepoints"], "@gitbook/fontawesome-pro": ["@gitbook/fontawesome-pro@1.0.8", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "^6.6.0" } }, "sha512-i4PgiuGyUb52Muhc52kK3aMJIMfMkA2RbPW30tre8a6M8T6mWTfYo6gafSgjNvF1vH29zcuB8oBYnF0gO4XcHA=="], diff --git a/packages/colors/.gitignore b/packages/colors/.gitignore new file mode 100644 index 0000000000..849ddff3b7 --- /dev/null +++ b/packages/colors/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/packages/colors/README.md b/packages/colors/README.md new file mode 100644 index 0000000000..cadefb7bbf --- /dev/null +++ b/packages/colors/README.md @@ -0,0 +1,3 @@ +# `@gitbook/colors` + +A set of default colors and transformation functions used throughout the GitBook Open and app. diff --git a/packages/colors/package.json b/packages/colors/package.json new file mode 100644 index 0000000000..77453a2a7f --- /dev/null +++ b/packages/colors/package.json @@ -0,0 +1,26 @@ +{ + "name": "@gitbook/colors", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "development": "./src/index.ts", + "default": "./dist/index.js" + } + }, + "version": "0.1.0", + "devDependencies": { + "typescript": "^5.5.3" + }, + "scripts": { + "build": "tsc", + "typecheck": "tsc --noEmit", + "dev": "tsc -w" + }, + "files": [ + "dist", + "src", + "README.md", + "CHANGELOG.md" + ] +} diff --git a/packages/colors/src/colors.ts b/packages/colors/src/colors.ts new file mode 100644 index 0000000000..7562febf63 --- /dev/null +++ b/packages/colors/src/colors.ts @@ -0,0 +1,39 @@ +/** + * Default primary color throughout the GitBook ecosystem. + */ +export const DEFAULT_PRIMARY_COLOR = '#346DDB'; + +/** + * The darkest color that exists in GitBook, used as the relative minimum of every generated color scale. + */ +export const DARK_BASE = '#1D1D1D'; + +/** + * The lightest color that exists in GitBook, used as the relative maximum of every generated color scale. + */ +export const LIGHT_BASE = '#FFFFFF'; + +/** + * Used as the basis of all UI elements that are not colored by the primary color. Neutral gray by default, overridden by site customization. + */ +export const DEFAULT_TINT_COLOR = '#787878'; + +/** + * Used for informational messages and neutral alerts. + */ +export const DEFAULT_HINT_INFO_COLOR = '#787878'; + +/** + * Used for showing important information or non-critical warnings. + */ +export const DEFAULT_HINT_WARNING_COLOR = '#FE9A00'; + +/** + * Used for destructive actions or raising attention to critical information. + */ +export const DEFAULT_HINT_DANGER_COLOR = '#FB2C36'; + +/** + * Used for showing positive actions or achievements. + */ +export const DEFAULT_HINT_SUCCESS_COLOR = '#00C950'; diff --git a/packages/colors/src/index.ts b/packages/colors/src/index.ts new file mode 100644 index 0000000000..3ae4e1ad64 --- /dev/null +++ b/packages/colors/src/index.ts @@ -0,0 +1,2 @@ +export * from './colors'; +export * from './transformations'; diff --git a/packages/gitbook/src/lib/colors.ts b/packages/colors/src/transformations.ts similarity index 90% rename from packages/gitbook/src/lib/colors.ts rename to packages/colors/src/transformations.ts index b289e73a3b..b361016668 100644 --- a/packages/gitbook/src/lib/colors.ts +++ b/packages/colors/src/transformations.ts @@ -1,3 +1,5 @@ +import { DARK_BASE, LIGHT_BASE, DEFAULT_TINT_COLOR } from './colors'; + type ColorShades = { [key: string]: string; }; @@ -6,9 +8,6 @@ type RGBColor = [number, number, number]; type OKLABColor = { L: number; A: number; B: number }; type OKLCHColor = { L: number; C: number; H: number }; -export const DARK_BASE = '#1d1d1d'; -export const LIGHT_BASE = '#ffffff'; -export const DEFAULT_TINT_COLOR = '#787878'; const D65 = [95.047, 100.0, 108.883]; // Reference white (D65) export enum ColorCategory { @@ -226,7 +225,7 @@ export function colorScale( /** * Convert a hex color to an RGB color set. */ -function hexToRgbArray(hex: string): RGBColor { +export function hexToRgbArray(hex: string): RGBColor { const originalHex = hex; let value = hex.replace('#', ''); @@ -252,7 +251,7 @@ function hexToRgbArray(hex: string): RGBColor { /** * Convert a RGB color set to a hex color. */ -function rgbArrayToHex(rgb: RGBColor): string { +export function rgbArrayToHex(rgb: RGBColor): string { return `#${rgb .map((channel) => { const component = channel.toString(16); @@ -262,7 +261,7 @@ function rgbArrayToHex(rgb: RGBColor): string { .join('')}`; } -function getColor(percentage: number, start: RGBColor, end: RGBColor) { +export function getColor(percentage: number, start: RGBColor, end: RGBColor) { const rgb = end.map((channel, index) => { return Math.round(channel + percentage * (start[index] - channel)); }); @@ -271,21 +270,21 @@ function getColor(percentage: number, start: RGBColor, end: RGBColor) { } // Utility constants and helper functions -function rgbToLinear(rgb: RGBColor): [number, number, number] { +export function rgbToLinear(rgb: RGBColor): [number, number, number] { return rgb.map((v) => { const scaled = v / 255; return scaled <= 0.04045 ? scaled / 12.92 : ((scaled + 0.055) / 1.055) ** 2.4; }) as [number, number, number]; } -function linearToRgb(linear: [number, number, number]): RGBColor { +export function linearToRgb(linear: [number, number, number]): RGBColor { return linear.map((v) => { const scaled = v <= 0.0031308 ? 12.92 * v : 1.055 * v ** (1 / 2.4) - 0.055; return Math.round(Math.max(0, Math.min(1, scaled)) * 255); }) as RGBColor; } -function rgbToOklab(rgb: RGBColor): OKLABColor { +export function rgbToOklab(rgb: RGBColor): OKLABColor { const [r, g, b] = rgbToLinear(rgb); const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b; @@ -303,7 +302,7 @@ function rgbToOklab(rgb: RGBColor): OKLABColor { }; } -function oklabToRgb(oklab: OKLABColor): RGBColor { +export function oklabToRgb(oklab: OKLABColor): RGBColor { const { L, A, B } = oklab; const lRoot = L + 0.3963377774 * A + 0.2158037573 * B; @@ -321,14 +320,14 @@ function oklabToRgb(oklab: OKLABColor): RGBColor { return linearToRgb([r, g, b]); } -function oklabToOklch(oklab: OKLABColor): OKLCHColor { +export function oklabToOklch(oklab: OKLABColor): OKLCHColor { const { L, A, B } = oklab; const C = Math.sqrt(A ** 2 + B ** 2); const H = (Math.atan2(B, A) * 180) / Math.PI; return { L, C, H: H < 0 ? H + 360 : H }; } -function oklchToOklab(oklch: OKLCHColor): OKLABColor { +export function oklchToOklab(oklch: OKLCHColor): OKLABColor { const { L, C, H } = oklch; const rad = (H * Math.PI) / 180; return { @@ -338,15 +337,15 @@ function oklchToOklab(oklch: OKLCHColor): OKLABColor { }; } -function rgbToOklch(rgb: RGBColor): OKLCHColor { +export function rgbToOklch(rgb: RGBColor): OKLCHColor { return oklabToOklch(rgbToOklab(rgb)); } -function oklchToRgb(oklch: OKLCHColor): RGBColor { +export function oklchToRgb(oklch: OKLCHColor): RGBColor { return oklabToRgb(oklchToOklab(oklch)); } -function rgbToXyz(rgb: RGBColor): [number, number, number] { +export function rgbToXyz(rgb: RGBColor): [number, number, number] { const [r, g, b] = rgbToLinear(rgb); return [ (r * 0.4124564 + g * 0.3575761 + b * 0.1804375) * 100, @@ -355,7 +354,11 @@ function rgbToXyz(rgb: RGBColor): [number, number, number] { ]; } -function xyzToLab65(xyz: [number, number, number]): { L: number; A: number; B: number } { +export function xyzToLab65(xyz: [number, number, number]): { + L: number; + A: number; + B: number; +} { const [x, y, z] = xyz.map((v, i) => { const scaled = v / D65[i]; return scaled > 0.008856 ? Math.cbrt(scaled) : 7.787 * scaled + 16 / 116; @@ -368,7 +371,7 @@ function xyzToLab65(xyz: [number, number, number]): { L: number; A: number; B: n }; } -function rgbTolab65(rgb: RGBColor): { L: number; A: number; B: number } { +export function rgbTolab65(rgb: RGBColor): { L: number; A: number; B: number } { return xyzToLab65(rgbToXyz(rgb)); } @@ -376,7 +379,7 @@ function rgbTolab65(rgb: RGBColor): { L: number; A: number; B: number } { Delta Phi Star perceptual lightness contrast by Andrew Somers: https://github.com/Myndex/deltaphistar */ -const PHI = 0.5 + Math.sqrt(1.25); +export const PHI = 0.5 + Math.sqrt(1.25); export function dpsContrast(a: RGBColor, b: RGBColor) { const dps = Math.abs(rgbTolab65(a).L ** PHI - rgbTolab65(b).L ** PHI); @@ -387,7 +390,10 @@ export function dpsContrast(a: RGBColor, b: RGBColor) { export function colorContrast(background: string, foreground: string[] = [LIGHT_BASE, DARK_BASE]) { const bg = hexToRgbArray(background); - const best: { color?: RGBColor; contrast: number } = { color: undefined, contrast: 0 }; + const best: { color?: RGBColor; contrast: number } = { + color: undefined, + contrast: 0, + }; for (const color of foreground) { const c = hexToRgbArray(color); diff --git a/packages/colors/tsconfig.json b/packages/colors/tsconfig.json new file mode 100644 index 0000000000..85cae73fd9 --- /dev/null +++ b/packages/colors/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": false, + "declaration": true, + "outDir": "dist", + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react", + "incremental": true, + "types": [ + "bun-types" // add Bun global + ] + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json index f7f5dc0994..ad4dc7010f 100644 --- a/packages/gitbook/package.json +++ b/packages/gitbook/package.json @@ -19,6 +19,7 @@ "dependencies": { "@gitbook/api": "^0.93.0", "@gitbook/cache-do": "workspace:*", + "@gitbook/colors": "workspace:*", "@gitbook/emoji-codepoints": "workspace:*", "@gitbook/icons": "workspace:*", "@gitbook/openapi-parser": "workspace:*", diff --git a/packages/gitbook/src/app/middleware/(site)/(core)/~gitbook/ogimage/[pageId]/route.tsx b/packages/gitbook/src/app/middleware/(site)/(core)/~gitbook/ogimage/[pageId]/route.tsx index 7840cef35f..196ea3ee92 100644 --- a/packages/gitbook/src/app/middleware/(site)/(core)/~gitbook/ogimage/[pageId]/route.tsx +++ b/packages/gitbook/src/app/middleware/(site)/(core)/~gitbook/ogimage/[pageId]/route.tsx @@ -1,11 +1,11 @@ import { CustomizationHeaderPreset } from '@gitbook/api'; +import { colorContrast } from '@gitbook/colors'; import { redirect } from 'next/navigation'; import { ImageResponse } from 'next/og'; import { NextRequest } from 'next/server'; import React from 'react'; import { googleFontsMap } from '@/fonts'; -import { colorContrast } from '@/lib/colors'; import { getAbsoluteHref } from '@/lib/links'; import { filterOutNullable } from '@/lib/typescript'; import { getContentTitle } from '@/lib/utils'; diff --git a/packages/gitbook/src/components/Ads/AdCoverRendering.tsx b/packages/gitbook/src/components/Ads/AdCoverRendering.tsx index 543193be9b..b6405165ba 100644 --- a/packages/gitbook/src/components/Ads/AdCoverRendering.tsx +++ b/packages/gitbook/src/components/Ads/AdCoverRendering.tsx @@ -1,7 +1,7 @@ import { SiteInsightsAd } from '@gitbook/api'; +import { hexToRgba } from '@gitbook/colors'; import * as React from 'react'; -import { hexToRgba } from '@/lib/colors'; import { getResizedImageURL } from '@/lib/images'; import { tcls } from '@/lib/tailwind'; diff --git a/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx b/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx index 1459fbde6b..8eed4f5863 100644 --- a/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx +++ b/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx @@ -9,18 +9,18 @@ import { type CustomizationTint, type SiteCustomizationSettings, } from '@gitbook/api'; -import { IconsProvider, IconStyle } from '@gitbook/icons'; - -import { fontNotoColorEmoji, fonts, ibmPlexMono } from '@/fonts'; -import { getSpaceLanguage } from '@/intl/server'; -import { getStaticFileURL } from '@/lib/assets'; import { colorContrast, colorScale, type ColorScaleOptions, DEFAULT_TINT_COLOR, hexToRgb, -} from '@/lib/colors'; +} from '@gitbook/colors'; +import { IconsProvider, IconStyle } from '@gitbook/icons'; + +import { fontNotoColorEmoji, fonts, ibmPlexMono } from '@/fonts'; +import { getSpaceLanguage } from '@/intl/server'; +import { getStaticFileURL } from '@/lib/assets'; import { tcls } from '@/lib/tailwind'; import { ClientContexts } from './ClientContexts'; diff --git a/packages/gitbook/tailwind.config.ts b/packages/gitbook/tailwind.config.ts index bff7348560..1a56691f3a 100644 --- a/packages/gitbook/tailwind.config.ts +++ b/packages/gitbook/tailwind.config.ts @@ -3,7 +3,7 @@ import typography from '@tailwindcss/typography'; import type { Config } from 'tailwindcss'; import plugin from 'tailwindcss/plugin'; -import { ColorCategory, hexToRgb, scale, shadesOfColor } from './src/lib/colors'; +import { ColorCategory, hexToRgb, scale, shadesOfColor } from '@gitbook/colors'; export const shades = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900]; export const opacities = [0, 4, 8, 12, 16, 24, 40, 64, 72, 88, 96, 100];