{"version":3,"file":"format-color.mjs","names":[],"sources":["../src/format-color.ts"],"sourcesContent":["import Color from 'colorjs.io';\nimport type { ColorFormat } from '#/color-formats.ts';\n\nexport interface NormalizedColor {\n  colorSpace: string;\n  components?: readonly (number | null)[];\n  channels?: readonly (number | null)[];\n  alpha?: number;\n  hex?: string;\n}\n\nexport interface FormatColorResult {\n  /** Display string — e.g. `rgb(59 132 246)`, `#3b82f6`. */\n  value: string;\n  /** True when the requested format can't losslessly represent the color. */\n  outOfGamut: boolean;\n}\n\nconst DEFAULT_FALLBACK = '—';\n\n/**\n * Render a normalized DTCG color payload as a display string in the\n * requested format, with a gamut flag. The shared rendering kernel behind\n * the blocks display surface and the MCP server (which add their own thin\n * wrappers). Pure — never throws; returns the `fallback` for unrecognized\n * input so callers don't need try/catch.\n *\n * `raw` here emits a compact normalized form; consumers that want the full\n * payload (the MCP server) handle `raw` in their own wrapper.\n */\nexport function formatColor(\n  value: unknown,\n  format: ColorFormat,\n  fallback: string = DEFAULT_FALLBACK,\n): FormatColorResult {\n  const normalized = coerce(value);\n  if (!normalized) return { value: stringifyFallback(value, fallback), outOfGamut: false };\n\n  if (format === 'raw') {\n    return { value: compactJson(normalized), outOfGamut: false };\n  }\n\n  const color = toColor(normalized);\n  if (!color) return { value: stringifyFallback(value, fallback), outOfGamut: false };\n\n  const alpha = typeof normalized.alpha === 'number' ? normalized.alpha : 1;\n\n  if (format === 'hex') return formatHex(color, alpha);\n  if (format === 'rgb') return formatRgb(color, alpha);\n  if (format === 'hsl') return formatHsl(color, alpha);\n  return formatOklch(color, alpha);\n}\n\n/**\n * Construct a colorjs.io `Color` from a normalized DTCG color payload,\n * applying the space-alias map so wide-gamut spaces (`display-p3`,\n * `a98-rgb`, `prophoto-rgb`) resolve. Returns null for unrecognized input.\n *\n * The shared primitive for consumers that need the color object itself —\n * perceptual sorting (oklch coords) or a gamut-correct CSS string — rather\n * than a pre-rendered display string. Replaces the hand-rolled, alias-map-\n * less colorjs construction that previously lived in each consumer.\n */\nexport function parseColor(value: unknown): Color | null {\n  const normalized = coerce(value);\n  if (!normalized) return null;\n  return toColor(normalized);\n}\n\n// Structural read of Terrazzo's normalized color shape (or a legacy\n// `channels`/`hex` payload) without a hard dep on @terrazzo/token-tools.\nfunction coerce(value: unknown): NormalizedColor | null {\n  if (!value || typeof value !== 'object') return null;\n  const v = value as Record<string, unknown>;\n  const colorSpace = typeof v['colorSpace'] === 'string' ? v['colorSpace'] : undefined;\n  const components = Array.isArray(v['components'])\n    ? (v['components'] as (number | null)[])\n    : Array.isArray(v['channels'])\n      ? (v['channels'] as (number | null)[])\n      : undefined;\n  if (!colorSpace || !components) {\n    if (typeof v['hex'] === 'string') {\n      const { components: hexComponents, alpha: hexAlpha } = hexToColor(v['hex']);\n      return {\n        colorSpace: 'srgb',\n        components: hexComponents,\n        ...(hexAlpha !== undefined && { alpha: hexAlpha }),\n      };\n    }\n    return null;\n  }\n  const alpha = typeof v['alpha'] === 'number' ? v['alpha'] : undefined;\n  const hexVal = v['hex'];\n  const hex = typeof hexVal === 'string' ? hexVal : undefined;\n  return {\n    colorSpace,\n    components,\n    ...(alpha !== undefined && { alpha }),\n    ...(hex !== undefined && { hex }),\n  };\n}\n\nfunction hexToColor(hex: string): { components: number[]; alpha?: number } {\n  const h = hex.replace('#', '');\n  const expanded =\n    h.length === 3 || h.length === 4\n      ? h\n          .split('')\n          .map((c) => c + c)\n          .join('')\n      : h;\n  const r = parseInt(expanded.slice(0, 2), 16) / 255;\n  const g = parseInt(expanded.slice(2, 4), 16) / 255;\n  const b = parseInt(expanded.slice(4, 6), 16) / 255;\n  // Preserve the alpha byte of #rgba / #rrggbbaa values instead of dropping it.\n  if (expanded.length >= 8) {\n    const a = parseInt(expanded.slice(6, 8), 16) / 255;\n    if (!Number.isNaN(a)) return { components: [r, g, b], alpha: a };\n  }\n  return { components: [r, g, b] };\n}\n\n// Map Terrazzo's canonical CSS Color 4 space identifiers to the shorter\n// identifiers colorjs.io registers. Only the ones that differ need an entry.\nconst COLORJS_SPACE_ALIASES: Record<string, string> = {\n  'display-p3': 'p3',\n  'a98-rgb': 'a98rgb',\n  'prophoto-rgb': 'prophoto',\n};\n\nfunction toColor(normalized: NormalizedColor): Color | null {\n  const source = normalized.components ?? normalized.channels ?? [];\n  const coords: [number, number, number] = [\n    numberOrZero(source[0]),\n    numberOrZero(source[1]),\n    numberOrZero(source[2]),\n  ];\n  const space = COLORJS_SPACE_ALIASES[normalized.colorSpace] ?? normalized.colorSpace;\n  try {\n    return new Color(space, coords, normalized.alpha ?? 1);\n  } catch {\n    return null;\n  }\n}\n\nfunction numberOrZero(n: number | null | undefined): number {\n  return typeof n === 'number' && !Number.isNaN(n) ? n : 0;\n}\n\nfunction coord(color: Color, i: number): number {\n  const c = color.coords[i];\n  return typeof c === 'number' && !Number.isNaN(c) ? c : 0;\n}\n\nfunction formatHex(color: Color, alpha: number): FormatColorResult {\n  const srgb = color.to('srgb');\n  const inGamut = srgb.inGamut('srgb');\n  if (!inGamut) {\n    const rgb = formatRgb(color, alpha);\n    return { value: rgb.value, outOfGamut: true };\n  }\n  const r = unitToByte(coord(srgb, 0));\n  const g = unitToByte(coord(srgb, 1));\n  const b = unitToByte(coord(srgb, 2));\n  const base = `#${toHexByte(r)}${toHexByte(g)}${toHexByte(b)}`;\n  if (alpha >= 1) return { value: base, outOfGamut: false };\n  const a = unitToByte(alpha);\n  return { value: `${base}${toHexByte(a)}`, outOfGamut: false };\n}\n\nfunction formatRgb(color: Color, alpha: number): FormatColorResult {\n  const srgb = color.to('srgb');\n  const inGamut = srgb.inGamut('srgb');\n  const r = Math.round(clampUnit(coord(srgb, 0)) * 255);\n  const g = Math.round(clampUnit(coord(srgb, 1)) * 255);\n  const b = Math.round(clampUnit(coord(srgb, 2)) * 255);\n  const body = `${r} ${g} ${b}`;\n  const value = alpha >= 1 ? `rgb(${body})` : `rgb(${body} / ${roundAlpha(alpha)})`;\n  return { value, outOfGamut: !inGamut };\n}\n\nfunction formatHsl(color: Color, alpha: number): FormatColorResult {\n  const hsl = color.to('hsl');\n  const srgb = color.to('srgb');\n  const inGamut = srgb.inGamut('srgb');\n  const hue = roundHue(coord(hsl, 0));\n  const sat = roundPercent(coord(hsl, 1));\n  const light = roundPercent(coord(hsl, 2));\n  const body = `${hue} ${sat}% ${light}%`;\n  const value = alpha >= 1 ? `hsl(${body})` : `hsl(${body} / ${roundAlpha(alpha)})`;\n  return { value, outOfGamut: !inGamut };\n}\n\nfunction formatOklch(color: Color, alpha: number): FormatColorResult {\n  const oklch = color.to('oklch');\n  const L = roundTo(coord(oklch, 0), 3);\n  const C = roundTo(coord(oklch, 1), 3);\n  const H = roundTo(coord(oklch, 2), 2);\n  const body = `${L} ${C} ${H}`;\n  const value = alpha >= 1 ? `oklch(${body})` : `oklch(${body} / ${roundAlpha(alpha)})`;\n  return { value, outOfGamut: false };\n}\n\nfunction unitToByte(n: number): number {\n  return Math.max(0, Math.min(255, Math.round(n * 255)));\n}\n\nfunction clampUnit(n: number): number {\n  return Math.max(0, Math.min(1, n));\n}\n\nfunction toHexByte(n: number): string {\n  return n.toString(16).padStart(2, '0');\n}\n\nfunction roundTo(n: number, digits: number): number {\n  const f = 10 ** digits;\n  return Math.round(n * f) / f;\n}\n\nfunction roundHue(h: number): number {\n  return roundTo(((h % 360) + 360) % 360, 1);\n}\n\nfunction roundPercent(n: number): number {\n  return Math.round(n * 10) / 10;\n}\n\nfunction roundAlpha(a: number): number {\n  return roundTo(a, 3);\n}\n\nfunction compactJson(value: NormalizedColor): string {\n  const parts: string[] = [`\"colorSpace\":${JSON.stringify(value.colorSpace)}`];\n  const components = value.components ?? value.channels;\n  if (components) {\n    parts.push(`\"components\":[${components.map((c) => (c === null ? 'null' : c)).join(', ')}]`);\n  }\n  if (typeof value.alpha === 'number' && value.alpha !== 1) {\n    parts.push(`\"alpha\":${value.alpha}`);\n  }\n  return `{ ${parts.join(', ')} }`;\n}\n\nfunction stringifyFallback(value: unknown, fallback: string): string {\n  if (value == null) return fallback;\n  if (typeof value === 'string' || typeof value === 'number') return String(value);\n  return fallback;\n}\n"],"mappings":"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"}