/** * Colour sources for conditional formatting rules. * * A rule paints one CSS value (`ConditionalRule.bgColor`), and this module is * where that value comes from: * * palette → `RULE_COLORS`, translucent tints over the row background * status → `--status-badge-*-fill`, the exact fills the status chips use, so * "highlight the overdue rows" and the Overdue badge agree. The * tokens are redefined per theme and per high-contrast mode, so the * fill flips with the surface instead of stranding light ink on a * light fill. * custom → the user's colour, wrapped at the palette's tint strength. Tinting * rather than filling is what keeps body text above the contrast * floor in both themes without pairing an ink per rule (P8). */ import type { StatusBadgeTone } from "./status-badge-tints" import { RULE_COLORS, type ConditionalRule, type ConditionalRuleColorSource, } from "./table-properties-types" export const CONDITIONAL_RULE_COLOR_SOURCES: { value: ConditionalRuleColorSource label: string }[] = [ { value: "palette", label: "Palette" }, { value: "status", label: "Status" }, { value: "custom", label: "Custom" }, ] /** Status badge fills, in the DS tone order (success · info · caution · danger · neutral). */ export const STATUS_RULE_COLORS: { name: string tone: StatusBadgeTone bg: string }[] = [ { name: "Success", tone: "success", bg: "var(--status-badge-success-fill)" }, { name: "Info", tone: "info", bg: "var(--status-badge-info-fill)" }, { name: "Caution", tone: "warning", bg: "var(--status-badge-warning-fill)" }, { name: "Danger", tone: "danger", bg: "var(--status-badge-danger-fill)" }, { name: "Neutral", tone: "neutral", bg: "var(--status-badge-neutral-fill)" }, ] /** Matches the palette tokens' own strength (`--conditional-rule-*` in globals.css). */ export const CUSTOM_RULE_TINT_PERCENT = 22 export const DEFAULT_CUSTOM_RULE_COLOR = "#2f6feb" export function customRuleBackground(color: string): string { return `color-mix(in oklch, ${color} ${CUSTOM_RULE_TINT_PERCENT}%, transparent)` } const CUSTOM_TINT_PATTERN = /^color-mix\(\s*in\s+oklch\s*,\s*(.+?)\s+\d+(?:\.\d+)?%\s*,\s*transparent\s*\)$/i const PLAIN_COLOR_PATTERN = /^(#[0-9a-f]{3,8}|rgb|rgba|hsl|hsla|oklch|oklab)/i /** The colour inside a custom tint, or `undefined` when `bg` is not one. */ export function customRuleColorFromBackground(bg: string): string | undefined { const match = CUSTOM_TINT_PATTERN.exec(bg.trim()) if (!match) return undefined const inner = match[1].trim() return inner.startsWith("var(") ? undefined : inner } /** * Which picker a rule belongs to. Rules saved before colour sources existed have * no `colorSource`, so their background is matched against the known palettes * and anything else is treated as a colour the user chose. */ export function resolveRuleColorSource( rule: Pick, ): ConditionalRuleColorSource { if (rule.colorSource) return rule.colorSource const bg = rule.bgColor?.trim() ?? "" if (STATUS_RULE_COLORS.some(c => c.bg === bg)) return "status" if (RULE_COLORS.some(c => c.bg === bg)) return "palette" if (customRuleColorFromBackground(bg)) return "custom" if (PLAIN_COLOR_PATTERN.test(bg)) return "custom" return "palette" } /** The colour a custom rule's picker should open on. */ export function ruleCustomColor( rule: Pick, ): string { if (rule.customColor?.trim()) return rule.customColor.trim() const bg = rule.bgColor?.trim() ?? "" return ( customRuleColorFromBackground(bg) ?? (PLAIN_COLOR_PATTERN.test(bg) ? bg : DEFAULT_CUSTOM_RULE_COLOR) ) } /** The patch that switches a rule to `source`, keeping a sensible colour. */ export function ruleColorSourcePatch( rule: Pick, source: ConditionalRuleColorSource, ): Partial { if (source === "palette") { const current = RULE_COLORS.find(c => c.bg === rule.bgColor) return { colorSource: source, bgColor: (current ?? RULE_COLORS[0]).bg } } if (source === "status") { const current = STATUS_RULE_COLORS.find(c => c.bg === rule.bgColor) return { colorSource: source, bgColor: (current ?? STATUS_RULE_COLORS[0]).bg } } const custom = ruleCustomColor(rule) return { colorSource: source, customColor: custom, bgColor: customRuleBackground(custom), } }