# Color Helpers

Tailwind-inspired color utility classes and a `colorHelper` for DOT components.

---

## File Structure

```
colorHelpers/
├── background/
│   └── backgroundColor.module.css   # Background color utilities
├── text/
│   └── textColor.module.css         # Text color utilities
├── border/
│   └── borderColor.module.css       # Border color utilities
├── constants/
│   └── index.js                     # PALETTE_CONFIG (palette → tone mapping)
├── colorHelper.js                   # getPaletteClasses + getBgClass, getTextClass, getBorderClass
└── paletteUtilities.README.md       # This file
```

---

## Available Colors

All three CSS files share the same 7 color tones:

| Tone | Token Family | Use Case |
|------|--------------|----------|
| **default** | `--zdt_v1_palette_default_*` | Default/neutral actions |
| **grey** | `--zdt_v1_palette_grey_*` | Secondary, muted elements |
| **blue** | `--zdt_v1_palette_blue_*` | Information, primary actions |
| **green** | `--zdt_v1_palette_green_*` | Success, positive status |
| **orange** | `--zdt_v1_palette_orange_*` | Warning, caution |
| **red** | `--zdt_v1_palette_red_*` | Error, danger, destructive actions |
| **yellow** | `--zdt_v1_palette_yellow_*` | Alerts, highlights |

---

## Available Utilities

### Background (`background/backgroundColor.module.css`)

Per color (`default`, `grey`, `blue`, `green`, `orange`, `red`, `yellow`):

| Class Pattern | Description |
|---------------|-------------|
| `.bg-{color}` | Base background |
| `.bg-{color}-light` | Light background variant |
| `.hover\:bg-{color}:hover` | Hover state |
| `.hover\:bg-{color}-light:hover` | Light hover state |
| `.focus\:bg-{color}:focus` | Focus state |
| `.focus\:bg-{color}-light:focus` | Light focus state |
| `.active\:bg-{color}:active` | Active state |
| `.active\:bg-{color}-light:active` | Light active state |
| `.selected\:bg-{color}` | Selected state |
| `.selected\:bg-{color}-light` | Light selected state |

**Utility classes:** `.bg-transparent`, `.bg-white`, `.bg-black`

### Text (`text/textColor.module.css`)

Per color (`default`, `grey`, `blue`, `green`, `orange`, `red`, `yellow`):

| Class Pattern | Description |
|---------------|-------------|
| `.text-{color}` | Base text color |
| `.hover\:text-{color}:hover` | Hover state |
| `.focus\:text-{color}:focus` | Focus state |
| `.active\:text-{color}:active` | Active state |
| `.selected\:text-{color}` | Selected state |

**Special:** `.text-brand-filled` (+ hover/focus/active/selected) — uses `--zdt_v1_palette_default_filled_text` for filled default backgrounds.

**Utility classes:** `.text-transparent`, `.text-white`, `.text-black`

> **Note:** Text has no `-light` variant. The `colorHelper` switches to `filledTextTone` (e.g., `white`) when the background is filled.

### Border (`border/borderColor.module.css`)

Per color (`default`, `grey`, `blue`, `green`, `orange`, `red`, `yellow`):

| Class Pattern | Description |
|---------------|-------------|
| `.border-{color}` | Base border |
| `.border-{color}-light` | Light border variant (resting state only) |
| `.hover\:border-{color}:hover` | Hover state |
| `.focus\:border-{color}:focus` | Focus state |
| `.active\:border-{color}:active` | Active state |
| `.selected\:border-{color}` | Selected state |

**Utility classes:** `.border-transparent`, `.border-white`, `.border-black`

> **Note:** Border state classes only use the base tone — no `-light` state variants. The `-light` class is available for the resting state only.

---

## Direct CSS Usage (Examples)

Import only the CSS modules you need and compose classes directly.

### Example 1: Status Tag (text + background)

```javascript
import bgStyle from './background/backgroundColor.module.css';
import textStyle from './text/textColor.module.css';

function StatusTag({ status }) {
  const className = compileClassNames({
    [bgStyle['bg-green-light']]: status === 'success',
    [bgStyle['bg-red-light']]: status === 'error',
    [bgStyle['bg-orange-light']]: status === 'warning',
    [textStyle['text-green']]: status === 'success',
    [textStyle['text-red']]: status === 'error',
    [textStyle['text-orange']]: status === 'warning',
  });

  return <span className={className}>{status}</span>;
}
```

### Example 2: Bordered Card (border + background)

```javascript
import bgStyle from './background/backgroundColor.module.css';
import borderStyle from './border/borderColor.module.css';

function Card({ children, variant }) {
  const className = compileClassNames({
    [bgStyle['bg-blue-light']]: variant === 'info',
    [bgStyle['bg-red-light']]: variant === 'error',
    [borderStyle['border-blue']]: variant === 'info',
    [borderStyle['border-red']]: variant === 'error',
  });

  return <div className={className}>{children}</div>;
}
```

### Example 3: Interactive Button (text + background + border + hover)

```javascript
import bgStyle from './background/backgroundColor.module.css';
import textStyle from './text/textColor.module.css';
import borderStyle from './border/borderColor.module.css';

function Button({ label, variant }) {
  // Primary: blue bg, white text, blue border
  const primaryClasses = compileClassNames({
    [bgStyle['bg-blue']]: true,
    [bgStyle['hover:bg-blue']]: true,
    [textStyle['text-white']]: true,
    [borderStyle['border-blue']]: true,
    [borderStyle['hover:border-blue']]: true,
  });

  // Danger: red bg, white text, red border
  const dangerClasses = compileClassNames({
    [bgStyle['bg-red']]: true,
    [bgStyle['hover:bg-red']]: true,
    [textStyle['text-white']]: true,
    [borderStyle['border-red']]: true,
    [borderStyle['hover:border-red']]: true,
  });

  return <button className={variant === 'danger' ? dangerClasses : primaryClasses}>{label}</button>;
}
```
---

## Using `colorHelper.js`

### Named Exports

Three utility functions return individual CSS module classes:

```javascript
import { getBgClass, getTextClass, getBorderClass } from './colorHelper';
```

#### `getBgClass({ state?, tone, isLighter? })`

Returns a background CSS class from `backgroundColor.module.css`.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `tone` | `string` | Yes | Color tone (`default`, `grey`, `blue`, `green`, `orange`, `red`, `yellow`, `white`, `black`) |
| `state` | `string` | No | State prefix (`hover`, `focus`, `active`, `selected`). Omit for base class. |
| `isLighter` | `boolean` | No | `true` → appends `-light` to the tone |

**Generated key pattern:** `[state:]bg-{tone}[-light]`

```javascript
getBgClass({ tone: 'blue' })                                  // → paletteBgStyle['bg-blue']
getBgClass({ tone: 'blue', isLighter: true })                  // → paletteBgStyle['bg-blue-light']
getBgClass({ state: 'hover', tone: 'red' })                    // → paletteBgStyle['hover:bg-red']
getBgClass({ state: 'hover', tone: 'red', isLighter: true })   // → paletteBgStyle['hover:bg-red-light']
```

#### `getTextClass({ state?, textTone })`

Returns a text CSS class from `textColor.module.css`.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `textTone` | `string` | Yes | Text tone (`brand`, `grey`, `blue`, `green`, `orange`, `red`, `yellow`, `white`, `black`, `brand-filled`) |
| `state` | `string` | No | State prefix (`hover`, `focus`, `active`, `selected`). Omit for base class. |

> **Note:** `textTone` has no `isLighter` equivalent. Use `filledTextTone` (e.g. `white`) directly when needed.

**Generated key pattern:** `[state:]text-{textTone}`

```javascript
getTextClass({ textTone: 'green' })                    // → paletteTextStyle['text-green']
getTextClass({ state: 'hover', textTone: 'white' })    // → paletteTextStyle['hover:text-white']
getTextClass({ textTone: 'brand-filled' })               // → paletteTextStyle['text-brand-filled']
```

#### `getBorderClass({ state?, tone, isLighter? })`

Returns a border CSS class from `borderColor.module.css`.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `tone` | `string` | Yes | Color tone (`default`, `grey`, `blue`, `green`, `orange`, `red`, `yellow`) |
| `state` | `string` | No | State prefix (`hover`, `focus`, `active`, `selected`). Omit for base class. |
| `isLighter` | `boolean` | No | `true` → appends `-light` to the tone (resting state only by convention) |

**Generated key pattern:** `[state:]border-{tone}[-light]`

```javascript
getBorderClass({ tone: 'blue' })                       // → paletteBorderStyle['border-blue']
getBorderClass({ tone: 'blue', isLighter: true })      // → paletteBorderStyle['border-blue-light']
getBorderClass({ state: 'hover', tone: 'red' })        // → paletteBorderStyle['hover:border-red']
```

#### Internal Helpers (not exported)

- **`toTone(tone, isLighter)`** — returns `'{tone}-light'` if `isLighter` is truthy, else `'{tone}'`
- **`withState(state, key)`** — returns `'{state}:{key}'` if `state` is truthy, else `'{key}'`

---

### Default Export: `getPaletteClasses`

For components accepting `palette`, `bgAppearance`, `borderAppearance`, and `paletteShade` props.

```javascript
import getPaletteClasses from './colorHelper';
```

#### Palette → Tone Mapping (from `PALETTE_CONFIG`)

| Palette | Tone (bg/border) | Text Tone | Filled Text Tone |
|---------|-------------------|-----------|-------------------|
| `default` | `brand` | `brand` | `brand-filled` |
| `primary` | `blue` | `blue` | `white` |
| `secondary` | `grey` | `grey` | `white` |
| `danger` | `red` | `red` | `white` |
| `success` | `green` | `green` | `white` |

Unknown palettes return `DUMMY_OBJECT` (empty frozen object).

#### Parameters

| Parameter | Values | Effect |
|-----------|--------|--------|
| `palette` | `default` \| `primary` \| `secondary` \| `danger` \| `success` | Selects the color tone config from `PALETTE_CONFIG` |
| `bgAppearance` | `'none'` \| `'default'` \| other | `'default'` → base bg + state bg; `'none'` → no bg, no state bg, no filled text logic; other → no base bg but state bg + filled text logic |
| `isSelected` | `boolean` | `true` → folds selected-state classes into `bg`, `text`, and `border` (replaces their resting values with the `selected` state class) |
| `borderAppearance` | `'none'` \| `'onHover'` \| other | `'none'` → transparent base, no state borders; `'onHover'` → transparent base, with state borders; other → visible base + state borders |
| `paletteShade` | `'lighter'` \| other | `'lighter'` → `-light` variant for bg and base border; also keeps text as `textTone` instead of `filledTextTone` |

#### Text Tone Logic

The text color depends on both `bgAppearance` and `paletteShade`:

| Condition | Base text (`text`) | State text (`hoverText`, etc.) |
|---|---|---|
| `bgAppearance: 'default'` + default shade | `filledTextTone` (e.g. `white`) | `filledTextTone` |
| `bgAppearance: 'default'` + `'lighter'` shade | `textTone` (e.g. `blue`) | `textTone` |
| `bgAppearance:` other (not `'none'`) + default shade | `textTone` | `filledTextTone` |
| `bgAppearance:` other (not `'none'`) + `'lighter'` shade | `textTone` | `textTone` |
| `bgAppearance: 'none'` | `textTone` | `textTone` |

> **Key insight:** `baseTextTone = isFilledBg && !isLighter ? filledTextTone : textTone`. `stateTextTone = hasBgState && !isLighter ? filledTextTone : textTone`. This means state text can differ from base text when `bgAppearance` is not `'default'` but also not `'none'`.

#### Return Value

```javascript
{
  bg,          // isSelected → selected bg (if bgAppearance !== 'none'); else base bg (only if bgAppearance === 'default')
  hoverBg,     // getBgClass({ state: 'hover', tone, isLighter }) — if bgAppearance !== 'none'
  focusBg,     // getBgClass({ state: 'focus', tone, isLighter }) — if bgAppearance !== 'none'
  activeBg,    // getBgClass({ state: 'active', tone, isLighter }) — if bgAppearance !== 'none'
  text,        // isSelected → selected text class; else getTextClass({ textTone: baseTextTone })
  hoverText,   // getTextClass({ state: 'hover', textTone: stateTextTone })
  focusText,   // getTextClass({ state: 'focus', textTone: stateTextTone })
  activeText,  // getTextClass({ state: 'active', textTone: stateTextTone })
  border,      // isSelected → selected border (if borderAppearance !== 'none'); else resting border
  hoverBorder, // getBorderClass({ state: 'hover', tone }) — if borderAppearance !== 'none'
  focusBorder, // getBorderClass({ state: 'focus', tone }) — if borderAppearance !== 'none'
  activeBorder // getBorderClass({ state: 'active', tone }) — if borderAppearance !== 'none'
}
```

> **Note:** State border classes always use the base tone (no `isLighter`), even when the resting border uses `-light`.

#### Example: Filled Primary Button

```javascript
const classes = getPaletteClasses({
  palette: 'primary',
  bgAppearance: 'default',
  borderAppearance: 'visible',
  paletteShade: 'default',
});
// classes.bg           → 'bg-blue'           (bgAppearance: 'default' + default shade)
// classes.hoverBg      → 'hover:bg-blue'
// classes.text         → 'text-white'        (filledTextTone — bgAppearance: 'default' + not lighter)
// classes.hoverText    → 'hover:text-white'  (filledTextTone — hasBgState + not lighter)
// classes.border       → 'border-blue'       (visible appearance)
// classes.hoverBorder  → 'hover:border-blue'
```

#### Example: Light Danger Tag

```javascript
const classes = getPaletteClasses({
  palette: 'danger',
  bgAppearance: 'default',
  borderAppearance: 'none',
  paletteShade: 'lighter',
});
// classes.bg           → 'bg-red-light'      (bgAppearance: 'default' + lighter shade)
// classes.hoverBg      → 'hover:bg-red-light'
// classes.text         → 'text-red'          (textTone — lighter shade overrides filledTextTone)
// classes.hoverText    → 'hover:text-red'    (textTone — lighter shade)
// classes.border       → 'border-transparent' (borderAppearance: 'none')
// classes.hoverBorder  → undefined           (no state borders with 'none')
```

#### Example: Ghost Secondary Button

```javascript
const classes = getPaletteClasses({
  palette: 'secondary',
  bgAppearance: 'none',
  borderAppearance: 'onHover',
  paletteShade: 'default',
});
// classes.bg           → undefined           (not filled)
// classes.hoverBg      → undefined           (bgAppearance: 'none')
// classes.text         → 'text-grey'         (textTone — not filled)
// classes.hoverText    → 'hover:text-grey'   (textTone — bgAppearance 'none' → no filled text)
// classes.border       → 'border-transparent' (onHover → transparent base)
// classes.hoverBorder  → 'hover:border-grey' (onHover → state borders present)
```

#### Example: Hover-fill Button (bgAppearance: non-standard value)

```javascript
const classes = getPaletteClasses({
  palette: 'primary',
  bgAppearance: 'hover-fill',  // any value that isn't 'none' or 'default'
  borderAppearance: 'visible',
  paletteShade: 'default',
});
// classes.bg           → undefined            (not 'default' → no base bg)
// classes.hoverBg      → 'hover:bg-blue'     (not 'none' → state bg present)
// classes.text         → 'text-blue'          (baseTextTone — not filled, so textTone)
// classes.hoverText    → 'hover:text-white'   (stateTextTone — hasBgState + not lighter → filledTextTone!)
// classes.border       → 'border-blue'        (visible)
// classes.hoverBorder  → 'hover:border-blue'
```

> This shows a key behavior: the base text is the normal tone, but **state text switches to `filledTextTone`** because the hover/state background will be filled (dark).

> **Note on `isSelected`:** Pass `isSelected: true` to fold the selected state into `bg`, `text`, and `border`. Instead of the resting class, each returns its `selected:` variant class. There are no separate `selectedBg`/`selectedText`/`selectedBorder` return keys.

---

## Behavior Summary

| Property | Lighter shade (`paletteShade: 'lighter'`) | Default shade |
|----------|-------------------------------------------|---------------|
| **Background** | Uses `-light` variant (e.g. `bg-blue-light`) | Uses base (e.g. `bg-blue`) |
| **Base text** | Always `textTone` (e.g. `text-blue`) | `filledTextTone` when filled (e.g. `text-white`), else `textTone` |
| **State text** | Always `textTone` (e.g. `hover:text-blue`) | `filledTextTone` when `bgAppearance !== 'none'` (e.g. `hover:text-white`), else `textTone` |
| **Border (base)** | Uses `-light` variant if not transparent (e.g. `border-blue-light`) | Uses base if not transparent (e.g. `border-blue`) |
| **Border (states)** | Always base tone (e.g. `hover:border-blue`) | Always base tone |

---

## FAQ

**Q: Why do text classes have no `-light` variant?**  
A: Text on a light background should stay the base color for readability. When the background is filled (dark), text switches to a contrasting tone (e.g. `white`) instead.

**Q: Why do border state classes not use `-light`?**  
A: Border hover/focus/active/selected states always use the full-intensity base tone for visibility, even when the resting border uses a lighter shade.

**Q: What happens if I pass an unknown palette to `getPaletteClasses`?**  
A: It returns `DUMMY_OBJECT` (an empty frozen object), so no classes are applied.

**Q: Can I use CSS classes directly without `getPaletteClasses`?**  
A: Yes. The CSS modules are standalone. `getPaletteClasses` is a convenience for components that accept palette/appearance/shade props.

**Q: When does state text differ from base text?**  
A: When `bgAppearance` is neither `'none'` nor `'filled'` with default (non-lighter) shade. Base text uses `textTone` (colored), but state text uses `filledTextTone` (e.g. `white`) — because the hover/state will show a filled background.

---

**Last Updated:** March 2026
