@getvitops/utils 0.9.0 → 2.0.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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["OKLCH_Space","sRGB_Space","P3_Space"],"sources":["../../src/color/colors.ts","../../src/color/color-harmony.ts"],"sourcesContent":["import Color from 'colorjs.io';\nimport {\n OKLCH as OKLCH_Space,\n sRGB as sRGB_Space,\n P3 as P3_Space,\n ColorSpace,\n to,\n toGamutCSS,\n clone,\n serialize,\n type PlainColorObject,\n} from 'colorjs.io/fn';\n\n// Register color spaces once for functional API\nColorSpace.register(OKLCH_Space);\nColorSpace.register(sRGB_Space);\nColorSpace.register(P3_Space);\n\nexport interface RgbResult {\n r: number;\n g: number;\n b: number;\n inGamut: boolean;\n /** Max channel overshoot beyond [0,1] before clamping. 0 when in gamut. */\n gamutDistance: number;\n}\n\n/**\n * Convert OKLCh to sRGB, returning 0-255 clamped values with gamut flag.\n * Fast path for per-pixel canvas rendering (no gamut mapping, just clamp).\n */\nexport function oklchToSrgb(l: number, c: number, h: number): RgbResult {\n const col: PlainColorObject = { space: OKLCH_Space, coords: [l, c, h], alpha: 1 };\n const rgb = to(col, sRGB_Space);\n const [r, g, b] = rgb.coords;\n const inGamut = r >= 0 && r <= 1 && g >= 0 && g <= 1 && b >= 0 && b <= 1;\n const gamutDistance = inGamut ? 0 : Math.max(-r, r - 1, -g, g - 1, -b, b - 1, 0);\n return {\n r: Math.max(0, Math.min(255, Math.round(r * 255))),\n g: Math.max(0, Math.min(255, Math.round(g * 255))),\n b: Math.max(0, Math.min(255, Math.round(b * 255))),\n inGamut,\n gamutDistance,\n };\n}\n\n/**\n * Convert OKLCh to Display P3, returning 0-255 clamped values with gamut flag.\n * Fast path for per-pixel canvas rendering on P3 displays.\n */\nexport function oklchToP3(l: number, c: number, h: number): RgbResult {\n const col: PlainColorObject = { space: OKLCH_Space, coords: [l, c, h], alpha: 1 };\n const rgb = to(col, P3_Space);\n const [r, g, b] = rgb.coords;\n const inGamut = r >= 0 && r <= 1 && g >= 0 && g <= 1 && b >= 0 && b <= 1;\n const gamutDistance = inGamut ? 0 : Math.max(-r, r - 1, -g, g - 1, -b, b - 1, 0);\n return {\n r: Math.max(0, Math.min(255, Math.round(r * 255))),\n g: Math.max(0, Math.min(255, Math.round(g * 255))),\n b: Math.max(0, Math.min(255, Math.round(b * 255))),\n inGamut,\n gamutDistance,\n };\n}\n\n/**\n * Gamut-map an OKLCh color using the CSS gamut mapping algorithm, return hex.\n * Slower than oklchToSrgb/oklchToP3 (uses iterative reduction), use for single colors only.\n */\nexport function gamutMapOklchToHex(\n l: number,\n c: number,\n h: number,\n space: 'srgb' | 'p3' = 'srgb',\n): string {\n const col: PlainColorObject = { space: OKLCH_Space, coords: [l, c, h], alpha: 1 };\n const targetSpace = space === 'p3' ? P3_Space : sRGB_Space;\n const mapped = toGamutCSS(clone(col), { space: targetSpace });\n const rgb = to(mapped, sRGB_Space);\n return serialize(rgb, { format: 'hex' });\n}\n\n// Standard 11-step scale - will be transposed based on input anchors\nexport const STEP_NAMES = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950] as const;\nexport type StepName = (typeof STEP_NAMES)[number];\n\nexport const COLOR_GROUPS = [\n 'neutral',\n 'brand',\n 'primary',\n 'accent',\n 'complement',\n 'success',\n 'warning',\n 'error',\n 'info',\n] as const;\nexport type ColorGroup = (typeof COLOR_GROUPS)[number];\n\n/**\n * Convert hex color to OKLCH components\n */\nexport function hexToOklch(hex: string): { l: number; c: number; h: number } {\n const color = new Color(hex);\n const oklch = color.to('oklch');\n return {\n l: oklch.coords[0],\n c: oklch.coords[1],\n h: oklch.coords[2] || 0,\n };\n}\n\n/**\n * Convert OKLCH components to hex color\n */\nexport function oklchToHex(oklch: { l: number; c: number; h: number }): string {\n const color = new Color('oklch', [oklch.l, oklch.c, oklch.h]);\n // Convert to sRGB and clamp to gamut, then format as hex\n const srgb = color.to('srgb');\n if (!srgb.inGamut()) {\n srgb.toGamut({ method: 'css' });\n }\n return srgb.toString({ format: 'hex' });\n}\n\n/**\n * Convert OKLCH CSS string to hex color\n * @example oklchStringToHex(\"oklch(50% 0.15 270)\") => \"#8b5cf6\"\n */\nexport function oklchStringToHex(oklchString: string): string {\n try {\n const color = new Color(oklchString);\n const srgb = color.to('srgb');\n if (!srgb.inGamut()) {\n srgb.toGamut({ method: 'css' });\n }\n return srgb.toString({ format: 'hex' });\n } catch {\n return '#808080';\n }\n}\n\n/**\n * Format a color name by separating compound words\n * Examples: \"mediumseagreen\" -> \"Medium Sea Green\", \"darkblue\" -> \"Dark Blue\"\n */\nexport function formatColorName(name: string): string {\n if (!name) return 'Unknown';\n\n // Handle special cases with proper word separation\n const specialCases: Record<string, string> = {\n aliceblue: 'Alice Blue',\n antiquewhite: 'Antique White',\n blanchedalmond: 'Blanched Almond',\n blueviolet: 'Blue Violet',\n burlywood: 'Burly Wood',\n cadetblue: 'Cadet Blue',\n cornflowerblue: 'Cornflower Blue',\n darkblue: 'Dark Blue',\n darkcyan: 'Dark Cyan',\n darkgoldenrod: 'Dark Goldenrod',\n darkgray: 'Dark Gray',\n darkgreen: 'Dark Green',\n darkgrey: 'Dark Grey',\n darkkhaki: 'Dark Khaki',\n darkmagenta: 'Dark Magenta',\n darkolivegreen: 'Dark Olive Green',\n darkorange: 'Dark Orange',\n darkorchid: 'Dark Orchid',\n darkred: 'Dark Red',\n darksalmon: 'Dark Salmon',\n darkseagreen: 'Dark Sea Green',\n darkslateblue: 'Dark Slate Blue',\n darkslategray: 'Dark Slate Gray',\n darkslategrey: 'Dark Slate Grey',\n darkturquoise: 'Dark Turquoise',\n darkviolet: 'Dark Violet',\n deeppink: 'Deep Pink',\n deepskyblue: 'Deep Sky Blue',\n dimgray: 'Dim Gray',\n dimgrey: 'Dim Grey',\n dodgerblue: 'Dodger Blue',\n floralwhite: 'Floral White',\n forestgreen: 'Forest Green',\n ghostwhite: 'Ghost White',\n greenyellow: 'Green Yellow',\n hotpink: 'Hot Pink',\n indianred: 'Indian Red',\n lavenderblush: 'Lavender Blush',\n lawngreen: 'Lawn Green',\n lemonchiffon: 'Lemon Chiffon',\n lightblue: 'Light Blue',\n lightcoral: 'Light Coral',\n lightcyan: 'Light Cyan',\n lightgoldenrodyellow: 'Light Goldenrod Yellow',\n lightgray: 'Light Gray',\n lightgreen: 'Light Green',\n lightgrey: 'Light Grey',\n lightpink: 'Light Pink',\n lightsalmon: 'Light Salmon',\n lightseagreen: 'Light Sea Green',\n lightskyblue: 'Light Sky Blue',\n lightslategray: 'Light Slate Gray',\n lightslategrey: 'Light Slate Grey',\n lightsteelblue: 'Light Steel Blue',\n lightyellow: 'Light Yellow',\n limegreen: 'Lime Green',\n mediumaquamarine: 'Medium Aquamarine',\n mediumblue: 'Medium Blue',\n mediumorchid: 'Medium Orchid',\n mediumpurple: 'Medium Purple',\n mediumseagreen: 'Medium Sea Green',\n mediumslateblue: 'Medium Slate Blue',\n mediumspringgreen: 'Medium Spring Green',\n mediumturquoise: 'Medium Turquoise',\n mediumvioletred: 'Medium Violet Red',\n midnightblue: 'Midnight Blue',\n mintcream: 'Mint Cream',\n mistyrose: 'Misty Rose',\n navajowhite: 'Navajo White',\n oldlace: 'Old Lace',\n olivedrab: 'Olive Drab',\n orangered: 'Orange Red',\n palegoldenrod: 'Pale Goldenrod',\n palegreen: 'Pale Green',\n paleturquoise: 'Pale Turquoise',\n palevioletred: 'Pale Violet Red',\n papayawhip: 'Papaya Whip',\n peachpuff: 'Peach Puff',\n powderblue: 'Powder Blue',\n rosybrown: 'Rosy Brown',\n royalblue: 'Royal Blue',\n saddlebrown: 'Saddle Brown',\n sandybrown: 'Sandy Brown',\n seagreen: 'Sea Green',\n skyblue: 'Sky Blue',\n slateblue: 'Slate Blue',\n slategray: 'Slate Gray',\n slategrey: 'Slate Grey',\n springgreen: 'Spring Green',\n steelblue: 'Steel Blue',\n whitesmoke: 'White Smoke',\n yellowgreen: 'Yellow Green',\n };\n\n const lowerName = name.toLowerCase();\n if (specialCases[lowerName]) {\n return specialCases[lowerName];\n }\n\n // Default: just capitalize first letter\n return name.charAt(0).toUpperCase() + name.slice(1);\n}\n\n/**\n * Get the closest CSS color name for a hex color\n */\nexport function getColorName(hex: string): string {\n try {\n const color = new Color(hex);\n const oklch = color.to('oklch');\n const hue = oklch.coords[2] || 0;\n\n if (hue < 30 || hue >= 330) return 'Red';\n if (hue < 60) return 'Orange';\n if (hue < 90) return 'Yellow';\n if (hue < 150) return 'Green';\n if (hue < 210) return 'Cyan';\n if (hue < 270) return 'Blue';\n if (hue < 330) return 'Purple';\n return 'Unknown';\n } catch {\n return 'Unknown';\n }\n}\n\n/**\n * Get color name with hue value\n */\nexport function getColorNameWithHue(hex: string): string {\n try {\n const color = new Color(hex);\n const oklch = color.to('oklch');\n const hue = oklch.coords[2] || 0;\n const roundedHue = Math.round(hue / 10) * 10;\n const name = getColorName(hex);\n return `${name} ${roundedHue}`;\n } catch {\n return 'Unknown';\n }\n}\n\n// Color harmony definitions\nexport const HARMONY_TYPES = [\n 'single',\n 'analogous',\n 'triad',\n 'complementary',\n 'split-complementary',\n 'square',\n 'compound',\n] as const;\nexport type HarmonyType = (typeof HARMONY_TYPES)[number];\n\nexport interface HarmonyDefinition {\n label: string;\n offsets: number[];\n primaryIndex: number;\n}\n\nexport const HARMONY_DEFINITIONS: Record<HarmonyType, HarmonyDefinition> = {\n single: {\n label: 'Single',\n offsets: [0],\n primaryIndex: 0,\n },\n analogous: {\n label: 'Analogous',\n offsets: [-30, 0, 30],\n primaryIndex: 1,\n },\n triad: {\n label: 'Triad',\n offsets: [0, 120, 240],\n primaryIndex: 0,\n },\n complementary: {\n label: 'Complementary',\n offsets: [0, 180],\n primaryIndex: 0,\n },\n 'split-complementary': {\n label: 'Split Complementary',\n offsets: [0, 150, 210],\n primaryIndex: 0,\n },\n square: {\n label: 'Square',\n offsets: [0, 90, 180, 270],\n primaryIndex: 0,\n },\n compound: {\n label: 'Compound',\n offsets: [-30, 0, 180, 210],\n primaryIndex: 1,\n },\n};\n\n// Default lightness positions (lightest to darkest)\nconst DEFAULT_LIGHTNESS_SCALE: Record<StepName, number> = {\n 50: 0.95,\n 100: 0.9,\n 200: 0.8,\n 300: 0.7,\n 400: 0.6,\n 500: 0.5,\n 600: 0.4,\n 700: 0.3,\n 800: 0.2,\n 900: 0.1,\n 950: 0.05,\n};\n\ninterface OklchColor {\n l: number;\n c: number;\n h: number;\n}\n\n/**\n * Parses an OKLCH CSS string into numerical components.\n * @param oklchString - e.g., 'oklch(50% 0.15 270)'\n * @returns Lightness (0-1), Chroma (0-~0.37), Hue (0-360)\n */\nexport function parseOklch(oklchString: string): OklchColor {\n const parts = oklchString.match(/oklch\\(([^ ]+) ([^ ]+) ([^ ]+)\\)/);\n if (!parts) throw new Error('Invalid OKLCH string format.');\n\n const [, lRaw = '', cRaw = '', hRaw = ''] = parts;\n const l = parseFloat(lRaw) / (lRaw.includes('%') ? 100 : 1);\n const c = parseFloat(cRaw);\n const h = parseFloat(hRaw);\n\n return { l, c, h };\n}\n\n/**\n * Linearly interpolates a value between a start and end point.\n */\nfunction lerp(start: number, end: number, factor: number): number {\n return start + factor * (end - start);\n}\n\n/**\n * Transpose the default lightness scale to match the provided anchors.\n */\nfunction transposeLightnessScale(\n parsedAnchors: Record<StepName, OklchColor>,\n sortedAnchorSteps: StepName[],\n): Record<StepName, number> {\n const transposedScale: Record<number, number> = {};\n\n if (sortedAnchorSteps.length === 1) {\n const anchorStep = sortedAnchorSteps[0] as StepName;\n const anchorL = (parsedAnchors[anchorStep] as OklchColor).l;\n const defaultAnchorL = DEFAULT_LIGHTNESS_SCALE[anchorStep];\n\n for (const step of STEP_NAMES) {\n if (step === anchorStep) {\n transposedScale[step] = anchorL;\n continue;\n }\n\n const defaultStepL = DEFAULT_LIGHTNESS_SCALE[step];\n\n if (defaultStepL > defaultAnchorL) {\n const factor = (defaultStepL - defaultAnchorL) / (1.0 - defaultAnchorL);\n transposedScale[step] = lerp(anchorL, 0.97, factor);\n } else {\n const factor = defaultStepL / defaultAnchorL;\n transposedScale[step] = lerp(0.05, anchorL, factor);\n }\n }\n\n return transposedScale;\n }\n\n for (const step of STEP_NAMES) {\n const anchored = parsedAnchors[step];\n if (anchored) {\n transposedScale[step] = anchored.l;\n continue;\n }\n\n let lowerStep: StepName | null = null;\n let upperStep: StepName | null = null;\n\n for (let i = sortedAnchorSteps.length - 1; i >= 0; i--) {\n const candidate = sortedAnchorSteps[i] as StepName;\n if (candidate < step) {\n lowerStep = candidate;\n break;\n }\n }\n\n for (const anchorStep of sortedAnchorSteps) {\n if (anchorStep > step) {\n upperStep = anchorStep;\n break;\n }\n }\n\n if (lowerStep !== null && upperStep !== null) {\n const lowerL = (parsedAnchors[lowerStep] as OklchColor).l;\n const upperL = (parsedAnchors[upperStep] as OklchColor).l;\n const span = upperStep - lowerStep;\n const position = step - lowerStep;\n const factor = position / span;\n transposedScale[step] = lerp(lowerL, upperL, factor);\n } else if (lowerStep !== null) {\n const lowerL = (parsedAnchors[lowerStep] as OklchColor).l;\n const defaultLowerL = DEFAULT_LIGHTNESS_SCALE[lowerStep];\n const defaultStepL = DEFAULT_LIGHTNESS_SCALE[step];\n const factor = defaultStepL / defaultLowerL;\n transposedScale[step] = lerp(0.05, lowerL, factor);\n } else if (upperStep !== null) {\n const upperL = (parsedAnchors[upperStep] as OklchColor).l;\n const defaultUpperL = DEFAULT_LIGHTNESS_SCALE[upperStep];\n const defaultStepL = DEFAULT_LIGHTNESS_SCALE[step];\n const factor = (defaultStepL - defaultUpperL) / (1.0 - defaultUpperL);\n transposedScale[step] = lerp(upperL, 0.97, factor);\n }\n }\n\n return transposedScale;\n}\n\n/**\n * Automatically assigns a color to the most appropriate step based on its lightness.\n */\nfunction assignStepByLightness(lightness: number): StepName {\n let closestStep: StepName = 500;\n let minDiff = Math.abs(lightness - DEFAULT_LIGHTNESS_SCALE[500]);\n\n for (const step of STEP_NAMES) {\n const diff = Math.abs(lightness - DEFAULT_LIGHTNESS_SCALE[step]);\n if (diff < minDiff) {\n minDiff = diff;\n closestStep = step;\n }\n }\n\n return closestStep;\n}\n\n/**\n * Generates a complete 11-step OKLCH palette from one or more colors.\n * Automatically determines which step each color belongs to based on lightness.\n */\nexport function generateOklchPalette(\n input: string | string[] | Record<string, string>,\n): Record<StepName, string> {\n let anchors: Record<number, string> = {};\n\n if (typeof input === 'string') {\n const parsed = parseOklch(input);\n const step = assignStepByLightness(parsed.l);\n anchors[step] = input;\n } else if (Array.isArray(input)) {\n for (const colorStr of input) {\n const parsed = parseOklch(colorStr);\n const step = assignStepByLightness(parsed.l);\n anchors[step] = colorStr;\n }\n } else if (typeof input === 'object') {\n anchors = input;\n } else {\n throw new Error(\n 'Input must be a color string, array of colors, or object with step assignments',\n );\n }\n\n const parsedAnchors: Record<number, OklchColor> = {};\n\n for (const key in anchors) {\n parsedAnchors[parseInt(key)] = parseOklch(anchors[key] as string);\n }\n\n const sortedAnchorSteps = Object.keys(parsedAnchors)\n .map(Number)\n .sort((a, b) => a - b) as StepName[];\n\n const lightnessScale = transposeLightnessScale(\n parsedAnchors as Record<StepName, OklchColor>,\n sortedAnchorSteps,\n );\n\n const palette: Record<number, string> = {};\n\n for (const step of STEP_NAMES) {\n if (parsedAnchors[step]) {\n palette[step] = anchors[step] as string;\n continue;\n }\n\n let startStep: StepName | null = null;\n let endStep: StepName | null = null;\n\n for (let i = sortedAnchorSteps.length - 1; i >= 0; i--) {\n const candidate = sortedAnchorSteps[i] as StepName;\n if (candidate < step) {\n startStep = candidate;\n break;\n }\n }\n\n for (const anchorStep of sortedAnchorSteps) {\n if (anchorStep > step) {\n endStep = anchorStep;\n break;\n }\n }\n\n let c_int, h_int;\n\n if (startStep !== null && endStep !== null) {\n const startColor = parsedAnchors[startStep] as OklchColor;\n const endColor = parsedAnchors[endStep] as OklchColor;\n const span = endStep - startStep;\n const position = step - startStep;\n const factor = position / span;\n\n c_int = lerp(startColor.c, endColor.c, factor);\n h_int = lerp(startColor.h, endColor.h, factor);\n } else if (startStep !== null) {\n const a = parsedAnchors[startStep] as OklchColor;\n c_int = a.c;\n h_int = a.h;\n } else if (endStep !== null) {\n const a = parsedAnchors[endStep] as OklchColor;\n c_int = a.c;\n h_int = a.h;\n } else {\n c_int = 0.1;\n h_int = 0;\n }\n\n const l_int = lightnessScale[step] as number;\n\n const dampeningFactor =\n l_int < 0.25\n ? 0.3 + (l_int / 0.25) * 0.7\n : l_int > 0.75\n ? 0.3 + ((1 - l_int) / 0.25) * 0.7\n : 1.0;\n\n c_int *= dampeningFactor;\n c_int = Math.min(c_int, 0.37);\n\n const finalL = (l_int * 100).toFixed(2) + '%';\n const finalC = c_int.toFixed(4);\n const finalH = h_int.toFixed(2);\n\n palette[step] = `oklch(${finalL} ${finalC} ${finalH})`;\n }\n\n return palette;\n}\n\n// ── Contrast + endpoints (for the functional-token layer) ────────────────────\n\n/** APCA lightness-contrast (Lc) magnitude between a foreground and background. */\nexport function contrastLc(fg: string, bg: string): number {\n return Math.abs(new Color(bg).contrast(fg, 'APCA'));\n}\n\n/**\n * Pick a tinted near-white or near-black foreground for best contrast on `bg`\n * (a whisper of the background's hue mixed into off-white/off-black).\n */\nexport function pickOn(bg: string, hue?: number): string {\n const h = hue ?? hexToOklch(bg).h;\n const white = oklchToHex({ l: 0.98, c: 0.01, h });\n const black = oklchToHex({ l: 0.16, c: 0.02, h });\n return contrastLc(white, bg) >= contrastLc(black, bg) ? white : black;\n}\n\n/**\n * The tinted off-white / off-black endpoints for a hue — white/black with a\n * whisper of the hue mixed in. These are the text/background tints every ramp\n * needs (and the one exception strict brand kits allow).\n */\nexport function tintedEndpoints(hue: number, chroma = 0.01): { light: string; dark: string } {\n return {\n light: oklchToHex({ l: 0.98, c: Math.min(chroma, 0.02), h: hue }),\n dark: oklchToHex({ l: 0.16, c: Math.min(chroma * 1.5, 0.03), h: hue }),\n };\n}\n","/**\n * Color harmony strategies for generating complementary colors\n */\n\nexport type HarmonyStrategy =\n | 'complement'\n | 'analogous-plus'\n | 'analogous-minus'\n | 'triadic-plus'\n | 'triadic-minus'\n | 'split-plus'\n | 'split-minus'\n | 'tetradic-plus'\n | 'tetradic-minus';\n\nexport interface HarmonyOption {\n id: HarmonyStrategy;\n label: string;\n description: string;\n hueShift: number;\n}\n\nexport const HARMONY_STRATEGIES: HarmonyOption[] = [\n {\n id: 'complement',\n label: 'Complement',\n description: 'Opposite on color wheel (high contrast)',\n hueShift: 180,\n },\n {\n id: 'analogous-plus',\n label: 'Analogous+',\n description: 'Adjacent colors (harmonious)',\n hueShift: 30,\n },\n {\n id: 'analogous-minus',\n label: 'Analogous-',\n description: 'Adjacent colors (harmonious)',\n hueShift: -30,\n },\n {\n id: 'triadic-plus',\n label: 'Triadic+',\n description: 'Evenly spaced (balanced)',\n hueShift: 120,\n },\n {\n id: 'triadic-minus',\n label: 'Triadic-',\n description: 'Evenly spaced (balanced)',\n hueShift: -120,\n },\n {\n id: 'split-plus',\n label: 'Split Complement+',\n description: 'Near opposite (softer contrast)',\n hueShift: 150,\n },\n {\n id: 'split-minus',\n label: 'Split Complement-',\n description: 'Near opposite (softer contrast)',\n hueShift: -150,\n },\n {\n id: 'tetradic-plus',\n label: 'Tetradic+',\n description: 'Square spacing (rich palette)',\n hueShift: 90,\n },\n {\n id: 'tetradic-minus',\n label: 'Tetradic-',\n description: 'Square spacing (rich palette)',\n hueShift: -90,\n },\n];\n\n/**\n * Applies a harmony strategy to a hue value\n */\nexport function applyHarmonyStrategy(baseHue: number, strategy: HarmonyStrategy): number {\n const harmonyOption = HARMONY_STRATEGIES.find((h) => h.id === strategy);\n if (!harmonyOption) return baseHue;\n\n let newHue = baseHue + harmonyOption.hueShift;\n\n // Normalize to 0-360 range\n while (newHue < 0) newHue += 360;\n while (newHue >= 360) newHue -= 360;\n\n return newHue;\n}\n\n/**\n * Gets all harmony suggestions for a given base hue\n */\nexport function getHarmonySuggestions(\n baseHue: number,\n): Array<{ strategy: HarmonyStrategy; hue: number; label: string; description: string }> {\n return HARMONY_STRATEGIES.map((strategy) => ({\n strategy: strategy.id,\n hue: applyHarmonyStrategy(baseHue, strategy.id),\n label: strategy.label,\n description: strategy.description,\n }));\n}\n\n/**\n * Default functional color hues (if user doesn't specify)\n */\nexport const DEFAULT_FUNCTIONAL_HUES = {\n success: 142, // Green\n warning: 38, // Orange/Amber\n error: 0, // Red\n info: 200, // Blue\n} as const;\n\n/**\n * Suggests a functional color hue based on existing palette to maintain harmony\n */\nexport function suggestFunctionalColorHue(\n type: keyof typeof DEFAULT_FUNCTIONAL_HUES,\n existingHues: number[],\n): number {\n const defaultHue = DEFAULT_FUNCTIONAL_HUES[type];\n\n if (existingHues.length === 0) {\n return defaultHue;\n }\n\n // Find the closest existing hue and adjust slightly to maintain harmony\n const avgHue = existingHues.reduce((sum, h) => sum + h, 0) / existingHues.length;\n\n // Rotate the default hue to be more in line with the palette's color temperature\n const hueShift = avgHue - 200; // Assuming 200 is neutral blue\n let adjustedHue = defaultHue + hueShift * 0.3; // 30% influence\n\n // Normalize\n while (adjustedHue < 0) adjustedHue += 360;\n while (adjustedHue >= 360) adjustedHue -= 360;\n\n return adjustedHue;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["OKLCH_Space","sRGB_Space","P3_Space"],"sources":["../../src/color/colors.ts","../../src/color/color-harmony.ts"],"sourcesContent":["import Color from 'colorjs.io';\nimport {\n OKLCH as OKLCH_Space,\n sRGB as sRGB_Space,\n P3 as P3_Space,\n ColorSpace,\n to,\n toGamutCSS,\n clone,\n serialize,\n type PlainColorObject,\n} from 'colorjs.io/fn';\n\n// Register color spaces once for functional API\nColorSpace.register(OKLCH_Space);\nColorSpace.register(sRGB_Space);\nColorSpace.register(P3_Space);\n\nexport interface RgbResult {\n r: number;\n g: number;\n b: number;\n inGamut: boolean;\n /** Max channel overshoot beyond [0,1] before clamping. 0 when in gamut. */\n gamutDistance: number;\n}\n\n/**\n * Convert OKLCh to sRGB, returning 0-255 clamped values with gamut flag.\n * Fast path for per-pixel canvas rendering (no gamut mapping, just clamp).\n */\nexport function oklchToSrgb(l: number, c: number, h: number): RgbResult {\n const col: PlainColorObject = { space: OKLCH_Space, coords: [l, c, h], alpha: 1 };\n const rgb = to(col, sRGB_Space);\n const [r, g, b] = rgb.coords;\n const inGamut = r >= 0 && r <= 1 && g >= 0 && g <= 1 && b >= 0 && b <= 1;\n const gamutDistance = inGamut ? 0 : Math.max(-r, r - 1, -g, g - 1, -b, b - 1, 0);\n return {\n r: Math.max(0, Math.min(255, Math.round(r * 255))),\n g: Math.max(0, Math.min(255, Math.round(g * 255))),\n b: Math.max(0, Math.min(255, Math.round(b * 255))),\n inGamut,\n gamutDistance,\n };\n}\n\n/**\n * Convert OKLCh to Display P3, returning 0-255 clamped values with gamut flag.\n * Fast path for per-pixel canvas rendering on P3 displays.\n */\nexport function oklchToP3(l: number, c: number, h: number): RgbResult {\n const col: PlainColorObject = { space: OKLCH_Space, coords: [l, c, h], alpha: 1 };\n const rgb = to(col, P3_Space);\n const [r, g, b] = rgb.coords;\n const inGamut = r >= 0 && r <= 1 && g >= 0 && g <= 1 && b >= 0 && b <= 1;\n const gamutDistance = inGamut ? 0 : Math.max(-r, r - 1, -g, g - 1, -b, b - 1, 0);\n return {\n r: Math.max(0, Math.min(255, Math.round(r * 255))),\n g: Math.max(0, Math.min(255, Math.round(g * 255))),\n b: Math.max(0, Math.min(255, Math.round(b * 255))),\n inGamut,\n gamutDistance,\n };\n}\n\n/**\n * Gamut-map an OKLCh color using the CSS gamut mapping algorithm, return hex.\n * Slower than oklchToSrgb/oklchToP3 (uses iterative reduction), use for single colors only.\n */\nexport function gamutMapOklchToHex(\n l: number,\n c: number,\n h: number,\n space: 'srgb' | 'p3' = 'srgb',\n): string {\n const col: PlainColorObject = { space: OKLCH_Space, coords: [l, c, h], alpha: 1 };\n const targetSpace = space === 'p3' ? P3_Space : sRGB_Space;\n const mapped = toGamutCSS(clone(col), { space: targetSpace });\n const rgb = to(mapped, sRGB_Space);\n return serialize(rgb, { format: 'hex' });\n}\n\n// Standard 11-step scale - will be transposed based on input anchors\nexport const STEP_NAMES = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950] as const;\nexport type StepName = (typeof STEP_NAMES)[number];\n\nexport const COLOR_GROUPS = [\n 'neutral',\n 'brand',\n 'primary',\n 'accent',\n 'complement',\n 'success',\n 'warning',\n 'error',\n 'info',\n] as const;\nexport type ColorGroup = (typeof COLOR_GROUPS)[number];\n\n/**\n * Convert hex color to OKLCH components\n */\nexport function hexToOklch(hex: string): { l: number; c: number; h: number } {\n const color = new Color(hex);\n const oklch = color.to('oklch');\n return {\n l: oklch.coords[0],\n c: oklch.coords[1],\n h: oklch.coords[2] || 0,\n };\n}\n\n/**\n * Convert OKLCH components to hex color\n */\nexport function oklchToHex(oklch: { l: number; c: number; h: number }): string {\n const color = new Color('oklch', [oklch.l, oklch.c, oklch.h]);\n // Convert to sRGB and clamp to gamut, then format as hex\n const srgb = color.to('srgb');\n if (!srgb.inGamut()) {\n srgb.toGamut({ method: 'css' });\n }\n return srgb.toString({ format: 'hex' });\n}\n\n/**\n * Convert OKLCH CSS string to hex color\n * @example oklchStringToHex(\"oklch(50% 0.15 270)\") => \"#8b5cf6\"\n */\nexport function oklchStringToHex(oklchString: string): string {\n try {\n const color = new Color(oklchString);\n const srgb = color.to('srgb');\n if (!srgb.inGamut()) {\n srgb.toGamut({ method: 'css' });\n }\n return srgb.toString({ format: 'hex' });\n } catch {\n return '#808080';\n }\n}\n\n/**\n * Format a color name by separating compound words\n * Examples: \"mediumseagreen\" -> \"Medium Sea Green\", \"darkblue\" -> \"Dark Blue\"\n */\nexport function formatColorName(name: string): string {\n if (!name) return 'Unknown';\n\n // Handle special cases with proper word separation\n const specialCases: Record<string, string> = {\n aliceblue: 'Alice Blue',\n antiquewhite: 'Antique White',\n blanchedalmond: 'Blanched Almond',\n blueviolet: 'Blue Violet',\n burlywood: 'Burly Wood',\n cadetblue: 'Cadet Blue',\n cornflowerblue: 'Cornflower Blue',\n darkblue: 'Dark Blue',\n darkcyan: 'Dark Cyan',\n darkgoldenrod: 'Dark Goldenrod',\n darkgray: 'Dark Gray',\n darkgreen: 'Dark Green',\n darkgrey: 'Dark Grey',\n darkkhaki: 'Dark Khaki',\n darkmagenta: 'Dark Magenta',\n darkolivegreen: 'Dark Olive Green',\n darkorange: 'Dark Orange',\n darkorchid: 'Dark Orchid',\n darkred: 'Dark Red',\n darksalmon: 'Dark Salmon',\n darkseagreen: 'Dark Sea Green',\n darkslateblue: 'Dark Slate Blue',\n darkslategray: 'Dark Slate Gray',\n darkslategrey: 'Dark Slate Grey',\n darkturquoise: 'Dark Turquoise',\n darkviolet: 'Dark Violet',\n deeppink: 'Deep Pink',\n deepskyblue: 'Deep Sky Blue',\n dimgray: 'Dim Gray',\n dimgrey: 'Dim Grey',\n dodgerblue: 'Dodger Blue',\n floralwhite: 'Floral White',\n forestgreen: 'Forest Green',\n ghostwhite: 'Ghost White',\n greenyellow: 'Green Yellow',\n hotpink: 'Hot Pink',\n indianred: 'Indian Red',\n lavenderblush: 'Lavender Blush',\n lawngreen: 'Lawn Green',\n lemonchiffon: 'Lemon Chiffon',\n lightblue: 'Light Blue',\n lightcoral: 'Light Coral',\n lightcyan: 'Light Cyan',\n lightgoldenrodyellow: 'Light Goldenrod Yellow',\n lightgray: 'Light Gray',\n lightgreen: 'Light Green',\n lightgrey: 'Light Grey',\n lightpink: 'Light Pink',\n lightsalmon: 'Light Salmon',\n lightseagreen: 'Light Sea Green',\n lightskyblue: 'Light Sky Blue',\n lightslategray: 'Light Slate Gray',\n lightslategrey: 'Light Slate Grey',\n lightsteelblue: 'Light Steel Blue',\n lightyellow: 'Light Yellow',\n limegreen: 'Lime Green',\n mediumaquamarine: 'Medium Aquamarine',\n mediumblue: 'Medium Blue',\n mediumorchid: 'Medium Orchid',\n mediumpurple: 'Medium Purple',\n mediumseagreen: 'Medium Sea Green',\n mediumslateblue: 'Medium Slate Blue',\n mediumspringgreen: 'Medium Spring Green',\n mediumturquoise: 'Medium Turquoise',\n mediumvioletred: 'Medium Violet Red',\n midnightblue: 'Midnight Blue',\n mintcream: 'Mint Cream',\n mistyrose: 'Misty Rose',\n navajowhite: 'Navajo White',\n oldlace: 'Old Lace',\n olivedrab: 'Olive Drab',\n orangered: 'Orange Red',\n palegoldenrod: 'Pale Goldenrod',\n palegreen: 'Pale Green',\n paleturquoise: 'Pale Turquoise',\n palevioletred: 'Pale Violet Red',\n papayawhip: 'Papaya Whip',\n peachpuff: 'Peach Puff',\n powderblue: 'Powder Blue',\n rosybrown: 'Rosy Brown',\n royalblue: 'Royal Blue',\n saddlebrown: 'Saddle Brown',\n sandybrown: 'Sandy Brown',\n seagreen: 'Sea Green',\n skyblue: 'Sky Blue',\n slateblue: 'Slate Blue',\n slategray: 'Slate Gray',\n slategrey: 'Slate Grey',\n springgreen: 'Spring Green',\n steelblue: 'Steel Blue',\n whitesmoke: 'White Smoke',\n yellowgreen: 'Yellow Green',\n };\n\n const lowerName = name.toLowerCase();\n if (specialCases[lowerName]) {\n return specialCases[lowerName];\n }\n\n // Default: just capitalize first letter\n return name.charAt(0).toUpperCase() + name.slice(1);\n}\n\n/**\n * Get the closest CSS color name for a hex color\n */\nexport function getColorName(hex: string): string {\n try {\n const color = new Color(hex);\n const oklch = color.to('oklch');\n const hue = oklch.coords[2] || 0;\n\n if (hue < 30 || hue >= 330) return 'Red';\n if (hue < 60) return 'Orange';\n if (hue < 90) return 'Yellow';\n if (hue < 150) return 'Green';\n if (hue < 210) return 'Cyan';\n if (hue < 270) return 'Blue';\n if (hue < 330) return 'Purple';\n return 'Unknown';\n } catch {\n return 'Unknown';\n }\n}\n\n/**\n * Get color name with hue value\n */\nexport function getColorNameWithHue(hex: string): string {\n try {\n const color = new Color(hex);\n const oklch = color.to('oklch');\n const hue = oklch.coords[2] || 0;\n const roundedHue = Math.round(hue / 10) * 10;\n const name = getColorName(hex);\n return `${name} ${roundedHue}`;\n } catch {\n return 'Unknown';\n }\n}\n\n// Color harmony definitions\nexport const HARMONY_TYPES = [\n 'single',\n 'analogous',\n 'triad',\n 'complementary',\n 'split-complementary',\n 'square',\n 'compound',\n] as const;\nexport type HarmonyType = (typeof HARMONY_TYPES)[number];\n\nexport interface HarmonyDefinition {\n label: string;\n offsets: number[];\n primaryIndex: number;\n}\n\nexport const HARMONY_DEFINITIONS: Record<HarmonyType, HarmonyDefinition> = {\n single: {\n label: 'Single',\n offsets: [0],\n primaryIndex: 0,\n },\n analogous: {\n label: 'Analogous',\n offsets: [-30, 0, 30],\n primaryIndex: 1,\n },\n triad: {\n label: 'Triad',\n offsets: [0, 120, 240],\n primaryIndex: 0,\n },\n complementary: {\n label: 'Complementary',\n offsets: [0, 180],\n primaryIndex: 0,\n },\n 'split-complementary': {\n label: 'Split Complementary',\n offsets: [0, 150, 210],\n primaryIndex: 0,\n },\n square: {\n label: 'Square',\n offsets: [0, 90, 180, 270],\n primaryIndex: 0,\n },\n compound: {\n label: 'Compound',\n offsets: [-30, 0, 180, 210],\n primaryIndex: 1,\n },\n};\n\n/**\n * The shared lightness ladder every ramp is built on (lightest → darkest).\n *\n * Fixed, NOT seed-derived. Two hues at the same step have to read equally light,\n * otherwise one contrast table cannot serve every hue and a `muted` variant means\n * something different depending on which role you ask. Only chroma and hue vary\n * between ramps.\n *\n * The previous engine transposed this curve onto each seed, which let relative\n * luminance at step 300 range from 0.253 (rust) to 0.384 (amber) across the\n * shipped palette — the drift this replaces.\n *\n * Anchored steps are the one deliberate exception: see `generateOklchPalette`.\n */\nexport const LIGHTNESS_LADDER: Record<StepName, number> = {\n 50: 0.98,\n 100: 0.95,\n 200: 0.9,\n 300: 0.83,\n 400: 0.74,\n 500: 0.65,\n 600: 0.54,\n 700: 0.47,\n 800: 0.38,\n 900: 0.29,\n 950: 0.21,\n};\n\n/**\n * Chroma a ramp decays *towards* beyond its outermost anchors, so the near-white\n * and near-black ends keep a whisper of the hue instead of going flat grey. The\n * dark end carries slightly more because chroma reads weaker against black.\n *\n * It is a CEILING, not a target — `Math.min(ENDPOINT_CHROMA.x, anchor.c)`. That\n * distinction is the whole reason a neutral is authorable. The interpolation\n * factor reaches exactly 1 at steps 50 and 950 for every anchor position, so as a\n * target it made the endpoints these constants *unconditionally*: seeds at chroma\n * 0.001, 0.002, 0.05 and 0.2 all produced a byte-identical step 50. A brand\n * wanting plain white with grey panels had no seed that worked, and chroma 0 was\n * worst of all — colorjs returns NaN hue for a true achromatic colour, which\n * `hexToOklch` collapses to 0, so the \"neutral\" came out pink.\n *\n * As a ceiling, a low-chroma seed simply stays low and a chroma-0 seed stays 0 at\n * every unanchored step, which makes the NaN-hue collapse invisible rather than\n * something `hexToOklch` has to solve (its `|| 0` is load-bearing for\n * `getColorName`).\n */\nexport const ENDPOINT_CHROMA = { light: 0.008, dark: 0.015 } as const;\n\ninterface OklchColor {\n l: number;\n c: number;\n h: number;\n}\n\n/**\n * Parses an OKLCH CSS string into numerical components.\n * @param oklchString - e.g., 'oklch(50% 0.15 270)'\n * @returns Lightness (0-1), Chroma (0-~0.37), Hue (0-360)\n */\nexport function parseOklch(oklchString: string): OklchColor {\n const parts = oklchString.match(/oklch\\(([^ ]+) ([^ ]+) ([^ ]+)\\)/);\n if (!parts) throw new Error('Invalid OKLCH string format.');\n\n const [, lRaw = '', cRaw = '', hRaw = ''] = parts;\n const l = parseFloat(lRaw) / (lRaw.includes('%') ? 100 : 1);\n const c = parseFloat(cRaw);\n const h = parseFloat(hRaw);\n\n return { l, c, h };\n}\n\n/**\n * Linearly interpolates a value between a start and end point.\n */\nfunction lerp(start: number, end: number, factor: number): number {\n return start + factor * (end - start);\n}\n\n/**\n * Interpolate hue the short way round the wheel. A ramp anchored at 350° and 10°\n * spans 20°, not 340° — the naive lerp sent it the long way through cyan.\n */\nfunction lerpHue(start: number, end: number, factor: number): number {\n const delta = ((((end - start) % 360) + 540) % 360) - 180;\n return (start + delta * factor + 360) % 360;\n}\n\n/**\n * The ladder step whose lightness is closest to `lightness` — i.e. where a\n * supplied colour naturally belongs.\n */\nexport function assignStepByLightness(lightness: number): StepName {\n let closestStep: StepName = 500;\n let minDiff = Math.abs(lightness - LIGHTNESS_LADDER[500]);\n\n for (const step of STEP_NAMES) {\n const diff = Math.abs(lightness - LIGHTNESS_LADDER[step]);\n if (diff < minDiff) {\n minDiff = diff;\n closestStep = step;\n }\n }\n\n return closestStep;\n}\n\n/**\n * How far an anchored colour sits from its step's ladder lightness.\n *\n * The generator surfaces this as a warning: a large deviation is legal (the\n * caller asked for that exact colour) but it means the hue will read heavier or\n * lighter than its siblings at that step, which is worth knowing rather than\n * discovering later.\n */\nexport function ladderDeviation(step: StepName, lightness: number): number {\n return lightness - LIGHTNESS_LADDER[step];\n}\n\n/** Absolute chroma ceiling — beyond this nothing survives gamut mapping anyway. */\nconst MAX_CHROMA = 0.37;\n\n/**\n * Build a complete 11-step OKLCH ramp from one or more anchor colours.\n *\n * **Ladder-first with exact anchors.** Every anchor is reproduced *verbatim* at\n * its step; every other step takes its lightness from `LIGHTNESS_LADDER`, with\n * chroma and hue interpolated between the flanking anchors and decayed toward\n * `ENDPOINT_CHROMA` beyond the outermost ones.\n *\n * The consequence is the point: a client's brand hex survives byte-for-byte,\n * while the other ten steps stay directly comparable to every other hue. The\n * deviation from the ladder is bounded and *local to anchored steps* — unlike\n * the previous engine, which warped the whole ramp to fit its anchors so every\n * step drifted.\n *\n * Chroma may come out beyond the sRGB gamut here; that is deliberate.\n * `oklchStringToHex` gamut-maps with the CSS algorithm, which reduces chroma\n * while holding lightness, so the ladder survives the conversion.\n */\nexport function generateOklchPalette(\n input: string | string[] | Record<string, string>,\n): Record<StepName, string> {\n let anchors: Record<number, string> = {};\n\n if (typeof input === 'string') {\n anchors[assignStepByLightness(parseOklch(input).l)] = input;\n } else if (Array.isArray(input)) {\n for (const colorStr of input) {\n anchors[assignStepByLightness(parseOklch(colorStr).l)] = colorStr;\n }\n } else if (typeof input === 'object' && input !== null) {\n anchors = { ...input };\n } else {\n throw new Error(\n 'Input must be a color string, array of colors, or object with step assignments',\n );\n }\n\n const parsedAnchors: Record<number, OklchColor> = {};\n for (const [key, value] of Object.entries(anchors)) {\n parsedAnchors[Number(key)] = parseOklch(value);\n }\n\n const anchorSteps = Object.keys(parsedAnchors)\n .map(Number)\n .sort((a, b) => a - b) as StepName[];\n\n // Interpolate across step *index*, not the numeric label: the labels aren't\n // evenly spaced (50→100 is a smaller jump than 100→200), so lerping on them\n // would bunch the ramp up at the light end.\n const idx = (step: number) => STEP_NAMES.indexOf(step as StepName);\n const lastIdx = STEP_NAMES.length - 1;\n\n const palette: Record<number, string> = {};\n\n for (const step of STEP_NAMES) {\n // An anchor is the caller's exact colour. Emit it untouched.\n if (parsedAnchors[step]) {\n palette[step] = anchors[step] as string;\n continue;\n }\n\n // `lighter` is the nearest anchor with a smaller step (50 is lightest);\n // `darker` the nearest with a larger one.\n const lighter = [...anchorSteps].reverse().find((s) => s < step);\n const darker = anchorSteps.find((s) => s > step);\n\n let chroma: number;\n let hue: number;\n\n if (lighter !== undefined && darker !== undefined) {\n const a = parsedAnchors[lighter] as OklchColor;\n const b = parsedAnchors[darker] as OklchColor;\n const factor = (idx(step) - idx(lighter)) / (idx(darker) - idx(lighter));\n chroma = lerp(a.c, b.c, factor);\n hue = lerpHue(a.h, b.h, factor);\n } else if (lighter !== undefined) {\n // Past the darkest anchor — fade toward the tinted near-black end.\n const a = parsedAnchors[lighter] as OklchColor;\n const factor = (idx(step) - idx(lighter)) / (lastIdx - idx(lighter));\n chroma = lerp(a.c, Math.min(ENDPOINT_CHROMA.dark, a.c), factor);\n hue = a.h;\n } else if (darker !== undefined) {\n // Past the lightest anchor — fade toward the tinted near-white end.\n const b = parsedAnchors[darker] as OklchColor;\n const factor = (idx(darker) - idx(step)) / idx(darker);\n chroma = lerp(b.c, Math.min(ENDPOINT_CHROMA.light, b.c), factor);\n hue = b.h;\n } else {\n chroma = 0.1;\n hue = 0;\n }\n\n const lightness = LIGHTNESS_LADDER[step];\n const finalL = (lightness * 100).toFixed(2) + '%';\n const finalC = Math.min(chroma, MAX_CHROMA).toFixed(4);\n const finalH = hue.toFixed(2);\n\n palette[step] = `oklch(${finalL} ${finalC} ${finalH})`;\n }\n\n return palette;\n}\n\n// ── Contrast + endpoints (for the functional-token layer) ────────────────────\n\n/** APCA lightness-contrast (Lc) magnitude between a foreground and background. */\nexport function contrastLc(fg: string, bg: string): number {\n return Math.abs(new Color(bg).contrast(fg, 'APCA'));\n}\n\n/**\n * Pick a tinted near-white or near-black foreground for best contrast on `bg`\n * (a whisper of the background's hue mixed into off-white/off-black).\n */\nexport function pickOn(bg: string, hue?: number): string {\n const h = hue ?? hexToOklch(bg).h;\n const white = oklchToHex({ l: 0.98, c: 0.01, h });\n const black = oklchToHex({ l: 0.16, c: 0.02, h });\n return contrastLc(white, bg) >= contrastLc(black, bg) ? white : black;\n}\n\n/**\n * The tinted off-white / off-black endpoints for a hue — white/black with a\n * whisper of the hue mixed in. These are the text/background tints every ramp\n * needs (and the one exception strict brand kits allow).\n *\n * Derived from `LIGHTNESS_LADDER` and `ENDPOINT_CHROMA` rather than its own\n * literals, so a strict tone kit's endpoints land on the same ladder rungs a\n * generated ramp would have used. This is the single definition — callers must\n * not hand-write their own near-white/near-black.\n */\nexport function tintedEndpoints(hue: number, chroma?: number): { light: string; dark: string } {\n return {\n light: oklchToHex({\n l: LIGHTNESS_LADDER[50],\n c: Math.min(chroma ?? ENDPOINT_CHROMA.light, 0.02),\n h: hue,\n }),\n dark: oklchToHex({\n l: LIGHTNESS_LADDER[950],\n c: Math.min((chroma ?? ENDPOINT_CHROMA.dark) * 1.5, 0.03),\n h: hue,\n }),\n };\n}\n","/**\n * Color harmony strategies for generating complementary colors\n */\n\nexport type HarmonyStrategy =\n | 'complement'\n | 'analogous-plus'\n | 'analogous-minus'\n | 'triadic-plus'\n | 'triadic-minus'\n | 'split-plus'\n | 'split-minus'\n | 'tetradic-plus'\n | 'tetradic-minus';\n\nexport interface HarmonyOption {\n id: HarmonyStrategy;\n label: string;\n description: string;\n hueShift: number;\n}\n\nexport const HARMONY_STRATEGIES: HarmonyOption[] = [\n {\n id: 'complement',\n label: 'Complement',\n description: 'Opposite on color wheel (high contrast)',\n hueShift: 180,\n },\n {\n id: 'analogous-plus',\n label: 'Analogous+',\n description: 'Adjacent colors (harmonious)',\n hueShift: 30,\n },\n {\n id: 'analogous-minus',\n label: 'Analogous-',\n description: 'Adjacent colors (harmonious)',\n hueShift: -30,\n },\n {\n id: 'triadic-plus',\n label: 'Triadic+',\n description: 'Evenly spaced (balanced)',\n hueShift: 120,\n },\n {\n id: 'triadic-minus',\n label: 'Triadic-',\n description: 'Evenly spaced (balanced)',\n hueShift: -120,\n },\n {\n id: 'split-plus',\n label: 'Split Complement+',\n description: 'Near opposite (softer contrast)',\n hueShift: 150,\n },\n {\n id: 'split-minus',\n label: 'Split Complement-',\n description: 'Near opposite (softer contrast)',\n hueShift: -150,\n },\n {\n id: 'tetradic-plus',\n label: 'Tetradic+',\n description: 'Square spacing (rich palette)',\n hueShift: 90,\n },\n {\n id: 'tetradic-minus',\n label: 'Tetradic-',\n description: 'Square spacing (rich palette)',\n hueShift: -90,\n },\n];\n\n/**\n * Applies a harmony strategy to a hue value\n */\nexport function applyHarmonyStrategy(baseHue: number, strategy: HarmonyStrategy): number {\n const harmonyOption = HARMONY_STRATEGIES.find((h) => h.id === strategy);\n if (!harmonyOption) return baseHue;\n\n let newHue = baseHue + harmonyOption.hueShift;\n\n // Normalize to 0-360 range\n while (newHue < 0) newHue += 360;\n while (newHue >= 360) newHue -= 360;\n\n return newHue;\n}\n\n/**\n * Gets all harmony suggestions for a given base hue\n */\nexport function getHarmonySuggestions(\n baseHue: number,\n): Array<{ strategy: HarmonyStrategy; hue: number; label: string; description: string }> {\n return HARMONY_STRATEGIES.map((strategy) => ({\n strategy: strategy.id,\n hue: applyHarmonyStrategy(baseHue, strategy.id),\n label: strategy.label,\n description: strategy.description,\n }));\n}\n\n/**\n * Default functional color hues (if user doesn't specify)\n */\nexport const DEFAULT_FUNCTIONAL_HUES = {\n success: 142, // Green\n warning: 38, // Orange/Amber\n error: 0, // Red\n info: 200, // Blue\n} as const;\n\n/**\n * Suggests a functional color hue based on existing palette to maintain harmony\n */\nexport function suggestFunctionalColorHue(\n type: keyof typeof DEFAULT_FUNCTIONAL_HUES,\n existingHues: number[],\n): number {\n const defaultHue = DEFAULT_FUNCTIONAL_HUES[type];\n\n if (existingHues.length === 0) {\n return defaultHue;\n }\n\n // Find the closest existing hue and adjust slightly to maintain harmony\n const avgHue = existingHues.reduce((sum, h) => sum + h, 0) / existingHues.length;\n\n // Rotate the default hue to be more in line with the palette's color temperature\n const hueShift = avgHue - 200; // Assuming 200 is neutral blue\n let adjustedHue = defaultHue + hueShift * 0.3; // 30% influence\n\n // Normalize\n while (adjustedHue < 0) adjustedHue += 360;\n while (adjustedHue >= 360) adjustedHue -= 360;\n\n return adjustedHue;\n}\n"],"mappings":"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"}
@@ -8,6 +8,19 @@ interface FaviconOptions {
8
8
  outputDir?: string;
9
9
  /** Run `oxipng` on the larger PNGs when available (default: true). */
10
10
  optimize?: boolean;
11
+ /**
12
+ * Background composited under the two MASKABLE outputs — `icon-mask.png` and
13
+ * `apple-touch-icon.png` (default `#ffffff`, matching the web manifest's
14
+ * `background_color`). The other outputs keep the source's transparency.
15
+ *
16
+ * Maskable means the OS crops the image to its own shape and expects full
17
+ * bleed, so transparency there is not "no background", it is whatever the
18
+ * launcher happens to composite onto — usually black. iOS discards alpha on the
19
+ * apple-touch-icon outright. Since both files sit a deliberately-sized logo on a
20
+ * larger canvas (the maskable safe zone), leaving that canvas transparent turned
21
+ * ~36% of one and ~40% of the other into a black frame on the home screen.
22
+ */
23
+ backgroundColor?: string;
11
24
  }
12
25
  /**
13
26
  * Generate the standard favicon set from an SVG/PNG source into `outputDir`:
@@ -1 +1 @@
1
- {"version":3,"file":"favicon.d.mts","names":[],"sources":["../src/favicon.ts"],"mappings":";UAiBiB,cAAA;EAAA;EAEf,MAAA;;EAEA,YAAA;EAFA;EAIA,SAAA;EAAA;EAEA,QAAA;AAAA;AAAQ;AAsBV;;;;AAtBU,iBAsBY,gBAAA,CAAiB,IAAA,EAAM,cAAA,GAAiB,OAAO;AAAA,UA8FpD,WAAA;EACf,GAAA;EACA,IAAA;EACA,IAAA;EACA,KAAA;AAAA;AAAA,UAGe,iBAAA;EAPW;EAS1B,MAAA;EAPA;EASA,QAAA;EAPA;EASA,IAAA;AAAA;AANF;;;;AAAA,iBAagB,YAAA,CAAa,IAAA,GAAM,iBAAA,GAAyB,WAAW;AAAA,UAWtD,kBAAA;EACf,IAAA;EACA,SAAA;EACA,UAAA;EACA,eAAA;EAf0B;EAiB1B,IAAA;AAAA;;iBAIc,eAAA,CAAgB,IAAA,EAAM,kBAAA,GAAqB,MAAM;;iBAmB3C,oBAAA,CAAqB,GAAA,UAAa,IAAA,EAAM,kBAAA,GAAqB,OAAO"}
1
+ {"version":3,"file":"favicon.d.mts","names":[],"sources":["../src/favicon.ts"],"mappings":";UAiBiB,cAAA;EAAA;EAEf,MAAA;;EAEA,YAAA;EAFA;EAIA,SAAA;EAAA;EAEA,QAAA;EAaA;;AAAe;AAsDjB;;;;;;;;AAAqE;EAtDnE,eAAA;AAAA;;;;;;iBAsDoB,gBAAA,CAAiB,IAAA,EAAM,cAAA,GAAiB,OAAO;AAAA,UAqGpD,WAAA;EACf,GAAA;EACA,IAAA;EACA,IAAA;EACA,KAAA;AAAA;AAAA,UAGe,iBAAA;EAIf;EAFA,MAAA;EAII;EAFJ,QAAA;EASc;EAPd,IAAA;AAAA;;;;;iBAOc,YAAA,CAAa,IAAA,GAAM,iBAAA,GAAyB,WAAW;AAAA,UAWtD,kBAAA;EACf,IAAA;EACA,SAAA;EACA,UAAA;EACA,eAAA;EAHA;EAKA,IAAA;AAAA;;iBAIc,eAAA,CAAgB,IAAA,EAAM,kBAAA,GAAqB,MAAM;;iBAuB3C,oBAAA,CAAqB,GAAA,UAAa,IAAA,EAAM,kBAAA,GAAqB,OAAO"}
package/dist/favicon.mjs CHANGED
@@ -15,6 +15,19 @@ import { join } from "node:path";
15
15
  * crush) is used when present on PATH and skipped otherwise — it is optional.
16
16
  */
17
17
  const exec = promisify(cp.exec);
18
+ /** `#rgb` / `#rrggbb` → a sharp background. Anything else is rejected loudly. */
19
+ function parseBackground(color) {
20
+ const hex = color.trim().replace(/^#/, "");
21
+ const full = hex.length === 3 ? [...hex].map((c) => c + c).join("") : hex.length === 6 ? hex : null;
22
+ if (full == null || !/^[0-9a-f]{6}$/i.test(full)) throw new Error(`favicon: backgroundColor must be a hex colour like "#ffffff" or "#fff" (got ${JSON.stringify(color)}).`);
23
+ const n = Number.parseInt(full, 16);
24
+ return {
25
+ r: n >> 16 & 255,
26
+ g: n >> 8 & 255,
27
+ b: n & 255,
28
+ alpha: 1
29
+ };
30
+ }
18
31
  let warnedNoOxipng = false;
19
32
  async function optimizePng(file, enabled) {
20
33
  if (!enabled) return;
@@ -28,6 +41,21 @@ async function optimizePng(file, enabled) {
28
41
  }
29
42
  }
30
43
  /**
44
+ * Does the source carry any transparency at all?
45
+ *
46
+ * Best-effort and deliberately quiet on failure — this only decides whether to
47
+ * print a warning, so a probe that throws must not take the build down with it.
48
+ * `stats().isOpaque` is sharp's own answer and covers the SVG case too, since it
49
+ * rasterises first.
50
+ */
51
+ async function hasTransparency(sharp, source) {
52
+ try {
53
+ return !(await sharp(source, { density: 384 }).stats()).isOpaque;
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+ /**
31
59
  * Generate the standard favicon set from an SVG/PNG source into `outputDir`:
32
60
  * `favicon.ico`, `icon-{16?,32,192,512}.png`, `apple-touch-icon.png`, `icon-mask.png`.
33
61
  * Returns the list of written file paths.
@@ -38,6 +66,7 @@ async function generateFavicons(opts) {
38
66
  if (!source.endsWith(".svg") && !source.endsWith(".png")) throw new Error("favicon: source must be an SVG or PNG file");
39
67
  const dir = (opts.outputDir ?? ".").replace(/\/+$/, "");
40
68
  const optimize = opts.optimize ?? true;
69
+ const maskBg = parseBackground(opts.backgroundColor ?? "#ffffff");
41
70
  mkdirSync(dir, { recursive: true });
42
71
  const out = (name) => join(dir, name);
43
72
  const written = [];
@@ -75,47 +104,28 @@ async function generateFavicons(opts) {
75
104
  if (lowResSource) icoSources.unshift(out("icon-16.png"));
76
105
  await writeFile(out("favicon.ico"), await pngToIco(icoSources));
77
106
  written.push(out("favicon.ico"));
107
+ if (opts.backgroundColor == null && await hasTransparency(sharp, source)) console.warn(`favicon: ${source} has transparency, and the maskable outputs (icon-mask.png, apple-touch-icon.png) must be opaque — the OS crops them to its own shape. Defaulting their background to #ffffff; set backgroundColor to choose.`);
78
108
  await sharp(out("icon-512.png")).resize(140, 140, {
79
109
  fit: "contain",
80
- background: {
81
- r: 0,
82
- g: 0,
83
- b: 0,
84
- alpha: 0
85
- }
110
+ background: maskBg
86
111
  }).extend({
87
112
  top: 20,
88
113
  bottom: 20,
89
114
  left: 20,
90
115
  right: 20,
91
- background: {
92
- r: 0,
93
- g: 0,
94
- b: 0,
95
- alpha: 0
96
- }
97
- }).png().toFile(out("apple-touch-icon.png"));
116
+ background: maskBg
117
+ }).flatten({ background: maskBg }).png().toFile(out("apple-touch-icon.png"));
98
118
  written.push(out("apple-touch-icon.png"));
99
119
  await sharp(out("icon-512.png")).resize(409, 409, {
100
120
  fit: "contain",
101
- background: {
102
- r: 0,
103
- g: 0,
104
- b: 0,
105
- alpha: 0
106
- }
121
+ background: maskBg
107
122
  }).extend({
108
123
  top: 52,
109
124
  bottom: 51,
110
125
  left: 52,
111
126
  right: 51,
112
- background: {
113
- r: 0,
114
- g: 0,
115
- b: 0,
116
- alpha: 0
117
- }
118
- }).png().toFile(out("icon-mask.png"));
127
+ background: maskBg
128
+ }).flatten({ background: maskBg }).png().toFile(out("icon-mask.png"));
119
129
  written.push(out("icon-mask.png"));
120
130
  await Promise.all([
121
131
  "icon-192.png",
@@ -169,12 +179,14 @@ function faviconManifest(opts) {
169
179
  {
170
180
  src: src("icon-192.png"),
171
181
  sizes: "192x192",
172
- type: "image/png"
182
+ type: "image/png",
183
+ purpose: "any"
173
184
  },
174
185
  {
175
186
  src: src("icon-512.png"),
176
187
  sizes: "512x512",
177
- type: "image/png"
188
+ type: "image/png",
189
+ purpose: "any"
178
190
  },
179
191
  {
180
192
  src: src("icon-mask.png"),
@@ -1 +1 @@
1
- {"version":3,"file":"favicon.mjs","names":[],"sources":["../src/favicon.ts"],"sourcesContent":["/**\n * Favicon generation — rasterize a source SVG/PNG into the standard favicon set.\n *\n * Ported from the repo's `lib/favicon.ts` so the logic lives in one place instead\n * of being copied per client. `sharp` + `png-to-ico` are loaded lazily (dynamic\n * import) so merely importing this module is cheap; the native `sharp` binary is\n * only touched when `generateFavicons` actually runs. `oxipng` (lossless PNG\n * crush) is used when present on PATH and skipped otherwise — it is optional.\n */\nimport { mkdirSync } from 'node:fs';\nimport { copyFile, writeFile } from 'node:fs/promises';\nimport { promisify } from 'node:util';\nimport cp from 'node:child_process';\nimport { join } from 'node:path';\n\nconst exec = promisify(cp.exec);\n\nexport interface FaviconOptions {\n /** Source image — an SVG or PNG. */\n source: string;\n /** Optional simplified source for the 16px icon. */\n lowResSource?: string;\n /** Directory to write into (default: current directory). */\n outputDir?: string;\n /** Run `oxipng` on the larger PNGs when available (default: true). */\n optimize?: boolean;\n}\n\nlet warnedNoOxipng = false;\nasync function optimizePng(file: string, enabled: boolean): Promise<void> {\n if (!enabled) return;\n try {\n await exec(`oxipng -o 3 \"${file}\"`);\n } catch {\n // oxipng missing or failed — lossless crush is best-effort, so skip quietly.\n if (!warnedNoOxipng) {\n warnedNoOxipng = true;\n console.warn('favicon: oxipng not available — skipping lossless PNG optimization.');\n }\n }\n}\n\n/**\n * Generate the standard favicon set from an SVG/PNG source into `outputDir`:\n * `favicon.ico`, `icon-{16?,32,192,512}.png`, `apple-touch-icon.png`, `icon-mask.png`.\n * Returns the list of written file paths.\n */\nexport async function generateFavicons(opts: FaviconOptions): Promise<string[]> {\n const { source, lowResSource } = opts;\n if (!source) throw new Error('favicon: no source path provided');\n if (!source.endsWith('.svg') && !source.endsWith('.png'))\n throw new Error('favicon: source must be an SVG or PNG file');\n\n const dir = (opts.outputDir ?? '.').replace(/\\/+$/, '');\n const optimize = opts.optimize ?? true;\n mkdirSync(dir, { recursive: true });\n const out = (name: string) => join(dir, name);\n\n const written: string[] = [];\n\n // Modern browsers prefer a scalable SVG favicon (and it supports dark-mode via\n // CSS media queries) — ship the source verbatim as favicon.svg when it is one.\n if (source.endsWith('.svg')) {\n await copyFile(source, out('favicon.svg'));\n written.push(out('favicon.svg'));\n }\n\n // Lazy-load the native/heavy deps only when actually generating.\n const [{ default: sharp }, { default: pngToIco }] = await Promise.all([\n import('sharp'),\n import('png-to-ico'),\n ]);\n\n // Rasterize the main source at each size. sharp reads SVG natively via libvips;\n // rendering at high density first keeps small-size edges crisp (ignored for PNG).\n await Promise.all(\n [32, 192, 512].map((size) =>\n sharp(source, { density: 384 })\n .resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })\n .png()\n .toFile(out(`icon-${size}.png`))\n .then(() => written.push(out(`icon-${size}.png`))),\n ),\n );\n\n // Optional low-res 16px variant.\n if (lowResSource) {\n await sharp(lowResSource, { density: 384 })\n .resize(16, 16, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })\n .png()\n .toFile(out('icon-16.png'));\n written.push(out('icon-16.png'));\n }\n\n // favicon.ico — sharp can't encode .ico, so png-to-ico packs the PNGs into the\n // multi-resolution container. Browsers pick the best size inside the file.\n const icoSources = [out('icon-32.png')];\n if (lowResSource) icoSources.unshift(out('icon-16.png'));\n await writeFile(out('favicon.ico'), await pngToIco(icoSources));\n written.push(out('favicon.ico'));\n\n // apple-touch-icon: 140x140 logo centered on a 180x180 transparent canvas.\n await sharp(out('icon-512.png'))\n .resize(140, 140, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })\n .extend({\n top: 20,\n bottom: 20,\n left: 20,\n right: 20,\n background: { r: 0, g: 0, b: 0, alpha: 0 },\n })\n .png()\n .toFile(out('apple-touch-icon.png'));\n written.push(out('apple-touch-icon.png'));\n\n // icon-mask (maskable): 409x409 logo centered on a 512x512 canvas.\n await sharp(out('icon-512.png'))\n .resize(409, 409, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })\n .extend({\n top: 52,\n bottom: 51,\n left: 52,\n right: 51,\n background: { r: 0, g: 0, b: 0, alpha: 0 },\n })\n .png()\n .toFile(out('icon-mask.png'));\n written.push(out('icon-mask.png'));\n\n // Lossless crush of the larger PNGs (best-effort; needs oxipng on PATH).\n await Promise.all(\n ['icon-192.png', 'icon-512.png', 'apple-touch-icon.png', 'icon-mask.png'].map((image) =>\n optimizePng(out(image), optimize),\n ),\n );\n\n return written;\n}\n\n// ── Head tags + web manifest (the \"tag layer\" over the generated image files) ──\n\nexport interface FaviconLink {\n rel: string;\n href: string;\n type?: string;\n sizes?: string;\n}\n\nexport interface FaviconTagOptions {\n /** Whether a favicon.svg was emitted (source was an SVG). */\n hasSvg?: boolean;\n /** Include the `<link rel=\"manifest\">` (default true). */\n manifest?: boolean;\n /** URL base the favicon files are served from (default '' → site root). */\n base?: string;\n}\n\n/**\n * The ordered `<link>` data matching what `generateFavicons` writes. Pure — no\n * I/O — so it can drive an Astro `<Head />`, the CLI, or plain HTML.\n */\nexport function faviconLinks(opts: FaviconTagOptions = {}): FaviconLink[] {\n const base = (opts.base ?? '').replace(/\\/+$/, '');\n const href = (f: string) => `${base}/${f}`;\n const links: FaviconLink[] = [{ rel: 'icon', href: href('favicon.ico'), sizes: '32x32' }];\n if (opts.hasSvg) links.push({ rel: 'icon', type: 'image/svg+xml', href: href('favicon.svg') });\n links.push({ rel: 'icon', type: 'image/png', sizes: '32x32', href: href('icon-32.png') });\n links.push({ rel: 'apple-touch-icon', href: href('apple-touch-icon.png') });\n if (opts.manifest !== false) links.push({ rel: 'manifest', href: href('site.webmanifest') });\n return links;\n}\n\nexport interface WebManifestOptions {\n name: string;\n shortName?: string;\n themeColor?: string;\n backgroundColor?: string;\n /** URL base the icon files are served from (default '' → site root). */\n base?: string;\n}\n\n/** Build the web-app-manifest object (icons match `generateFavicons` output). */\nexport function faviconManifest(opts: WebManifestOptions): Record<string, unknown> {\n const base = (opts.base ?? '').replace(/\\/+$/, '');\n const src = (f: string) => `${base}/${f}`;\n return {\n name: opts.name,\n short_name: opts.shortName ?? opts.name,\n icons: [\n { src: src('icon-192.png'), sizes: '192x192', type: 'image/png' },\n { src: src('icon-512.png'), sizes: '512x512', type: 'image/png' },\n { src: src('icon-mask.png'), sizes: '512x512', type: 'image/png', purpose: 'maskable' },\n ],\n theme_color: opts.themeColor ?? '#ffffff',\n background_color: opts.backgroundColor ?? '#ffffff',\n display: 'standalone',\n start_url: '/',\n };\n}\n\n/** Write `site.webmanifest` into `dir`. Returns the written path. */\nexport async function writeFaviconManifest(dir: string, opts: WebManifestOptions): Promise<string> {\n const path = join(dir.replace(/\\/+$/, ''), 'site.webmanifest');\n await writeFile(path, `${JSON.stringify(faviconManifest(opts), null, 2)}\\n`);\n return path;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"favicon.mjs","names":[],"sources":["../src/favicon.ts"],"sourcesContent":["/**\n * Favicon generation — rasterize a source SVG/PNG into the standard favicon set.\n *\n * Ported from the repo's `lib/favicon.ts` so the logic lives in one place instead\n * of being copied per client. `sharp` + `png-to-ico` are loaded lazily (dynamic\n * import) so merely importing this module is cheap; the native `sharp` binary is\n * only touched when `generateFavicons` actually runs. `oxipng` (lossless PNG\n * crush) is used when present on PATH and skipped otherwise — it is optional.\n */\nimport { mkdirSync } from 'node:fs';\nimport { copyFile, writeFile } from 'node:fs/promises';\nimport { promisify } from 'node:util';\nimport cp from 'node:child_process';\nimport { join } from 'node:path';\n\nconst exec = promisify(cp.exec);\n\nexport interface FaviconOptions {\n /** Source image — an SVG or PNG. */\n source: string;\n /** Optional simplified source for the 16px icon. */\n lowResSource?: string;\n /** Directory to write into (default: current directory). */\n outputDir?: string;\n /** Run `oxipng` on the larger PNGs when available (default: true). */\n optimize?: boolean;\n /**\n * Background composited under the two MASKABLE outputs — `icon-mask.png` and\n * `apple-touch-icon.png` (default `#ffffff`, matching the web manifest's\n * `background_color`). The other outputs keep the source's transparency.\n *\n * Maskable means the OS crops the image to its own shape and expects full\n * bleed, so transparency there is not \"no background\", it is whatever the\n * launcher happens to composite onto — usually black. iOS discards alpha on the\n * apple-touch-icon outright. Since both files sit a deliberately-sized logo on a\n * larger canvas (the maskable safe zone), leaving that canvas transparent turned\n * ~36% of one and ~40% of the other into a black frame on the home screen.\n */\n backgroundColor?: string;\n}\n\n/** `#rgb` / `#rrggbb` → a sharp background. Anything else is rejected loudly. */\nfunction parseBackground(color: string): { r: number; g: number; b: number; alpha: number } {\n const hex = color.trim().replace(/^#/, '');\n const full =\n hex.length === 3 ? [...hex].map((c) => c + c).join('') : hex.length === 6 ? hex : null;\n if (full == null || !/^[0-9a-f]{6}$/i.test(full))\n throw new Error(\n `favicon: backgroundColor must be a hex colour like \"#ffffff\" or \"#fff\" (got ${JSON.stringify(color)}).`,\n );\n const n = Number.parseInt(full, 16);\n return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255, alpha: 1 };\n}\n\nlet warnedNoOxipng = false;\nasync function optimizePng(file: string, enabled: boolean): Promise<void> {\n if (!enabled) return;\n try {\n await exec(`oxipng -o 3 \"${file}\"`);\n } catch {\n // oxipng missing or failed — lossless crush is best-effort, so skip quietly.\n if (!warnedNoOxipng) {\n warnedNoOxipng = true;\n console.warn('favicon: oxipng not available — skipping lossless PNG optimization.');\n }\n }\n}\n\n/**\n * Does the source carry any transparency at all?\n *\n * Best-effort and deliberately quiet on failure — this only decides whether to\n * print a warning, so a probe that throws must not take the build down with it.\n * `stats().isOpaque` is sharp's own answer and covers the SVG case too, since it\n * rasterises first.\n */\nasync function hasTransparency(\n sharp: typeof import('sharp').default,\n source: string,\n): Promise<boolean> {\n try {\n return !(await sharp(source, { density: 384 }).stats()).isOpaque;\n } catch {\n return false;\n }\n}\n\n/**\n * Generate the standard favicon set from an SVG/PNG source into `outputDir`:\n * `favicon.ico`, `icon-{16?,32,192,512}.png`, `apple-touch-icon.png`, `icon-mask.png`.\n * Returns the list of written file paths.\n */\nexport async function generateFavicons(opts: FaviconOptions): Promise<string[]> {\n const { source, lowResSource } = opts;\n if (!source) throw new Error('favicon: no source path provided');\n if (!source.endsWith('.svg') && !source.endsWith('.png'))\n throw new Error('favicon: source must be an SVG or PNG file');\n\n const dir = (opts.outputDir ?? '.').replace(/\\/+$/, '');\n const optimize = opts.optimize ?? true;\n // Same default as the web manifest's `background_color`. The two disagreeing is\n // the actual bug: `backgroundColor` reached the manifest and never the raster.\n const maskBg = parseBackground(opts.backgroundColor ?? '#ffffff');\n mkdirSync(dir, { recursive: true });\n const out = (name: string) => join(dir, name);\n\n const written: string[] = [];\n\n // Modern browsers prefer a scalable SVG favicon (and it supports dark-mode via\n // CSS media queries) — ship the source verbatim as favicon.svg when it is one.\n if (source.endsWith('.svg')) {\n await copyFile(source, out('favicon.svg'));\n written.push(out('favicon.svg'));\n }\n\n // Lazy-load the native/heavy deps only when actually generating.\n const [{ default: sharp }, { default: pngToIco }] = await Promise.all([\n import('sharp'),\n import('png-to-ico'),\n ]);\n\n // Rasterize the main source at each size. sharp reads SVG natively via libvips;\n // rendering at high density first keeps small-size edges crisp (ignored for PNG).\n await Promise.all(\n [32, 192, 512].map((size) =>\n sharp(source, { density: 384 })\n .resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })\n .png()\n .toFile(out(`icon-${size}.png`))\n .then(() => written.push(out(`icon-${size}.png`))),\n ),\n );\n\n // Optional low-res 16px variant.\n if (lowResSource) {\n await sharp(lowResSource, { density: 384 })\n .resize(16, 16, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })\n .png()\n .toFile(out('icon-16.png'));\n written.push(out('icon-16.png'));\n }\n\n // favicon.ico — sharp can't encode .ico, so png-to-ico packs the PNGs into the\n // multi-resolution container. Browsers pick the best size inside the file.\n const icoSources = [out('icon-32.png')];\n if (lowResSource) icoSources.unshift(out('icon-16.png'));\n await writeFile(out('favicon.ico'), await pngToIco(icoSources));\n written.push(out('favicon.ico'));\n\n // The two MASKABLE outputs below are composited onto an opaque background: the\n // OS crops them to its own shape, so transparency reads as a black frame rather\n // than as \"no background\". Warn when we're inventing that colour rather than\n // being told it, since white is a guess that a dark logo would lose against.\n if (opts.backgroundColor == null && (await hasTransparency(sharp, source))) {\n console.warn(\n `favicon: ${source} has transparency, and the maskable outputs (icon-mask.png, ` +\n 'apple-touch-icon.png) must be opaque — the OS crops them to its own shape. ' +\n 'Defaulting their background to #ffffff; set backgroundColor to choose.',\n );\n }\n\n // apple-touch-icon: 140x140 logo centered on a 180x180 canvas.\n await sharp(out('icon-512.png'))\n .resize(140, 140, { fit: 'contain', background: maskBg })\n .extend({ top: 20, bottom: 20, left: 20, right: 20, background: maskBg })\n .flatten({ background: maskBg }) // iOS discards alpha; do it ourselves so we pick the colour\n .png()\n .toFile(out('apple-touch-icon.png'));\n written.push(out('apple-touch-icon.png'));\n\n // icon-mask (maskable): 409x409 logo centered on a 512x512 canvas. The 409/512\n // inset IS the maskable safe zone — the geometry was always right, only the\n // fill was wrong.\n await sharp(out('icon-512.png'))\n .resize(409, 409, { fit: 'contain', background: maskBg })\n .extend({ top: 52, bottom: 51, left: 52, right: 51, background: maskBg })\n .flatten({ background: maskBg })\n .png()\n .toFile(out('icon-mask.png'));\n written.push(out('icon-mask.png'));\n\n // Lossless crush of the larger PNGs (best-effort; needs oxipng on PATH).\n await Promise.all(\n ['icon-192.png', 'icon-512.png', 'apple-touch-icon.png', 'icon-mask.png'].map((image) =>\n optimizePng(out(image), optimize),\n ),\n );\n\n return written;\n}\n\n// ── Head tags + web manifest (the \"tag layer\" over the generated image files) ──\n\nexport interface FaviconLink {\n rel: string;\n href: string;\n type?: string;\n sizes?: string;\n}\n\nexport interface FaviconTagOptions {\n /** Whether a favicon.svg was emitted (source was an SVG). */\n hasSvg?: boolean;\n /** Include the `<link rel=\"manifest\">` (default true). */\n manifest?: boolean;\n /** URL base the favicon files are served from (default '' → site root). */\n base?: string;\n}\n\n/**\n * The ordered `<link>` data matching what `generateFavicons` writes. Pure — no\n * I/O — so it can drive an Astro `<Head />`, the CLI, or plain HTML.\n */\nexport function faviconLinks(opts: FaviconTagOptions = {}): FaviconLink[] {\n const base = (opts.base ?? '').replace(/\\/+$/, '');\n const href = (f: string) => `${base}/${f}`;\n const links: FaviconLink[] = [{ rel: 'icon', href: href('favicon.ico'), sizes: '32x32' }];\n if (opts.hasSvg) links.push({ rel: 'icon', type: 'image/svg+xml', href: href('favicon.svg') });\n links.push({ rel: 'icon', type: 'image/png', sizes: '32x32', href: href('icon-32.png') });\n links.push({ rel: 'apple-touch-icon', href: href('apple-touch-icon.png') });\n if (opts.manifest !== false) links.push({ rel: 'manifest', href: href('site.webmanifest') });\n return links;\n}\n\nexport interface WebManifestOptions {\n name: string;\n shortName?: string;\n themeColor?: string;\n backgroundColor?: string;\n /** URL base the icon files are served from (default '' → site root). */\n base?: string;\n}\n\n/** Build the web-app-manifest object (icons match `generateFavicons` output). */\nexport function faviconManifest(opts: WebManifestOptions): Record<string, unknown> {\n const base = (opts.base ?? '').replace(/\\/+$/, '');\n const src = (f: string) => `${base}/${f}`;\n return {\n name: opts.name,\n short_name: opts.shortName ?? opts.name,\n // `purpose` is stated on all three rather than left to default. An entry with\n // no purpose is implicitly \"any\", so omitting it worked — but with a maskable\n // present and nothing else claiming \"any\", some launchers picked the maskable\n // for slots that wanted a plain icon, i.e. the one with the safe-zone inset.\n icons: [\n { src: src('icon-192.png'), sizes: '192x192', type: 'image/png', purpose: 'any' },\n { src: src('icon-512.png'), sizes: '512x512', type: 'image/png', purpose: 'any' },\n { src: src('icon-mask.png'), sizes: '512x512', type: 'image/png', purpose: 'maskable' },\n ],\n theme_color: opts.themeColor ?? '#ffffff',\n background_color: opts.backgroundColor ?? '#ffffff',\n display: 'standalone',\n start_url: '/',\n };\n}\n\n/** Write `site.webmanifest` into `dir`. Returns the written path. */\nexport async function writeFaviconManifest(dir: string, opts: WebManifestOptions): Promise<string> {\n const path = join(dir.replace(/\\/+$/, ''), 'site.webmanifest');\n await writeFile(path, `${JSON.stringify(faviconManifest(opts), null, 2)}\\n`);\n return path;\n}\n"],"mappings":"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"}