node-colors.ts4.0 KBView on GitHub
/**
 * The closed rendering vocabulary for a node's background tint.
 *
 * Exactly the shape `BOARD_OPTION_ICONS` is, and for the same reason: the schema says
 * `{ champion: 'green' }`, so Cedar knows what `green` looks like and never learns what
 * `champion` means. Which field paints is `view.colorField`; what each of its values is worth
 * is `field.optionColors`, set by whoever authored the schema — because "which of my values
 * means good news" is a judgement about the values, and the graph is the only thing that knows.
 *
 * ── An unrecognised name renders as NO tint ──
 *
 * Never a fallback colour. The whole value of the tint is that it is read at a glance without
 * checking the legend, so a wrong-but-present colour is worse than none: an uncoloured node
 * reads as "not categorised", a mis-coloured one reads as a fact.
 *
 * ── Why these are SOLID, not a wash ──
 *
 * They were `bg-<hue>-500/12` — a 12% tint over whatever was behind the card. On a canvas that
 * is what it sounds like: the node reads as transparent, the dot grid shows through it, and the
 * category is a suggestion rather than a colour. A node is an OBJECT on the canvas and has to
 * look like one, so each tint is an opaque surface: a pale shade in light mode, a deep one in
 * dark, both chosen to leave `text-foreground` legible without a per-tint foreground colour.
 *
 * The border is the SAME hue only a step or two off the fill — barely a seam. It was several
 * steps stronger, which drew a hard ring around every card and made a chart of eight people
 * read as eight competing objects rather than as one diagram. The fill already carries the
 * category; the border only has to stop the card dissolving into the canvas behind it.
 */

export interface NodeColor {
  /**
   * The card's opaque background. Kept SEPARATE from the border so a selected node can take the
   * primary border without fighting this one — `border-primary` and `dark:border-green-800` are
   * different variants, so tailwind-merge cannot resolve them against each other and the tint
   * would win back in dark mode.
   */
  surfaceClassName: string;
  /** The same hue, stronger. Applied only when the node is not selected. */
  borderClassName: string;
  /** The legend's swatch — the same hue, solid, because a 12% chip is invisible at 12px. */
  swatchClassName: string;
  /** What a reader would call it. */
  label: string;
}

export const GRAPH_NODE_COLORS: Record<string, NodeColor> = {
  green: {
    surfaceClassName: 'bg-green-100 dark:bg-green-950',
    borderClassName: 'border-green-200 dark:border-green-900',
    swatchClassName: 'bg-green-500',
    label: 'Green',
  },
  blue: {
    surfaceClassName: 'bg-blue-100 dark:bg-blue-950',
    borderClassName: 'border-blue-200 dark:border-blue-900',
    swatchClassName: 'bg-blue-500',
    label: 'Blue',
  },
  violet: {
    surfaceClassName: 'bg-violet-100 dark:bg-violet-950',
    borderClassName: 'border-violet-200 dark:border-violet-900',
    swatchClassName: 'bg-violet-500',
    label: 'Violet',
  },
  cyan: {
    surfaceClassName: 'bg-cyan-100 dark:bg-cyan-950',
    borderClassName: 'border-cyan-200 dark:border-cyan-900',
    swatchClassName: 'bg-cyan-500',
    label: 'Cyan',
  },
  amber: {
    surfaceClassName: 'bg-amber-100 dark:bg-amber-950',
    borderClassName: 'border-amber-200 dark:border-amber-900',
    swatchClassName: 'bg-amber-500',
    label: 'Amber',
  },
  red: {
    surfaceClassName: 'bg-red-100 dark:bg-red-950',
    borderClassName: 'border-red-200 dark:border-red-900',
    swatchClassName: 'bg-red-500',
    label: 'Red',
  },
  slate: {
    surfaceClassName: 'bg-slate-100 dark:bg-slate-900',
    borderClassName: 'border-slate-200 dark:border-slate-800',
    swatchClassName: 'bg-slate-500',
    label: 'Slate',
  },
};

export const GRAPH_NODE_COLOR_NAMES = Object.keys(GRAPH_NODE_COLORS);

/** The tint for a colour name, or `undefined` for one Cedar does not know. */
export function nodeColorFor(name: string | undefined): NodeColor | undefined {
  return name ? GRAPH_NODE_COLORS[name] : undefined;
}