Files
suaveplan/openspec/specs/colours/spec.md
2026-05-27 23:29:53 +01:00

171 lines
6.0 KiB
Markdown

<!-- markdownlint-disable MD024 -->
# Capability Spec: colours
**Package**: `@suaveplan/colours`
**Status**: STABLE
> Every requirement below carries a `**Verified by:**` line at authoring time.
## ADDED Requirements
### Requirement: Stable error code registry via defineCodes
The package SHALL declare a single `defineCodes("COLOUR", [...])` registry named `COLOUR_CODES`. No hand-typed string literals for codes SHALL appear in `src/` or tests.
#### Scenario: Code values use COLOUR.* namespace
- **WHEN** reading `COLOUR_CODES.INVALID_INPUT`
- **THEN** the value is exactly `"COLOUR.INVALID_INPUT"`
**Verified by:** `src/error-codes/error-codes.test.ts::"code values use COLOUR.* namespace"`
---
### Requirement: Typed error hierarchy extending SuaveplanError
The package SHALL export `ColourError` extending `SuaveplanError<ColourCode>` with typed factory functions for every failure mode.
#### Scenario: Factory functions produce correctly-coded errors
- **WHEN** calling `colourInvalid("bad hex")`
- **THEN** the result has code `COLOUR.INVALID_INPUT` and message context
**Verified by:** `src/errors/errors.test.ts::"factory functions produce correctly-coded errors"`
---
### Requirement: Colour parsing from multiple formats
The package SHALL parse colour input from hex (3/4/6/8 digit), modern CSS `rgb()`, `hsl()`, `lab()`, `lch()`, `oklab()`, `oklch()`, named colours, and plain RGB/RGBA/HSL/HSLA/HSV/LAB/LCH/OKLAB/OKLCH objects.
#### Scenario: Parse 6-digit hex
- **WHEN** parsing `"#ff9900"`
- **THEN** the result is `{ r: 255, g: 153, b: 0, a: 1 }`
#### Scenario: Parse modern space-separated rgb with alpha
- **WHEN** parsing `"rgb(255 128 0 / 0.5)"`
- **THEN** the result alpha is `0.5`
#### Scenario: Parse named colour
- **WHEN** parsing `"transparent"`
- **THEN** the result is `{ r: 0, g: 0, b: 0, a: 0 }`
#### Scenario: Reject invalid input
- **WHEN** parsing `"not-a-colour"`
- **THEN** it throws `ColourError` with code `COLOUR.INVALID_INPUT`
**Verified by:** `src/parser/parser.test.ts::"parses hex"` AND `src/parser/parser.test.ts::"parses modern rgb with alpha"` AND `src/parser/parser.test.ts::"parses named colours"` AND `src/parser/parser.test.ts::"rejects invalid input"`
---
### Requirement: Colour space conversions
The package SHALL provide bidirectional conversions between RGB, HSL, HSV, XYZ, LAB, LCH, OKLAB, OKLCH, and CMYK.
#### Scenario: RGB to HSL round-trip
- **WHEN** converting `{ r: 255, g: 128, b: 0 }` to HSL and back
- **THEN** the result is approximately the original RGB values
#### Scenario: LAB to LCH preserves chroma and hue
- **WHEN** converting `{ l: 50, a: 20, b: -30 }` to LCH
- **THEN** `c` and `h` are positive finite numbers
**Verified by:** `src/conversions/conversions.test.ts::"rgb to hsl round-trip"` AND `src/conversions/conversions.test.ts::"lab to lch preserves chroma and hue"`
---
### Requirement: Chainable Colour class manipulation
The package SHALL provide a `Colour` class with immutable chainable methods: `lighten`, `darken`, `saturate`, `desaturate`, `rotate`, `alpha`, `fade`, `opaquer`, `negate`, `grayscale`, and `mix`.
#### Scenario: Lighten and darken are inverse
- **WHEN** `colour.lighten(20).darken(20)`
- **THEN** the result approximately equals the original
#### Scenario: Mix in oklab space
- **WHEN** mixing red and blue with weight 50 in oklab
- **THEN** the result is a perceptually uniform mid-tone
**Verified by:** `src/colour/colour.test.ts::"lighten and darken are inverse"` AND `src/colour/colour.test.ts::"mix in oklab space"`
---
### Requirement: Colour analysis and comparison
The package SHALL compute luminance (WCAG), perceived brightness, contrast ratio, isDark/isLight, bestContrast selection, and distance in euclidean/CIE76/CIE94/oklab spaces.
#### Scenario: Contrast ratio between black and white
- **WHEN** computing contrast between black and white
- **THEN** the result is `21`
#### Scenario: bestContrast selects highest ratio
- **WHEN** calling `bestContrast` with black and white against red
- **THEN** the result is the colour with the highest contrast ratio
**Verified by:** `src/colour/colour.test.ts::"contrast ratio between black and white"` AND `src/colour/colour.test.ts::"bestContrast selects highest ratio"`
---
### Requirement: Output formatters
The package SHALL format colours to hex (3/6/8), rgb, rgba, hsl, hsla, hsv, lab, lch, oklab, oklch, cmyk, and named.
#### Scenario: Hex round-trip
- **WHEN** formatting a parsed hex colour back to hex
- **THEN** the result matches the canonical 6-digit form
**Verified by:** `src/colour/colour.test.ts::"hex round-trip"`
---
### Requirement: Colour factories
The package SHALL provide `randomColour`, `colourFromTemperature`, `blendColours`, `colourSteps`, and `generatePalette`.
#### Scenario: Steps produces correct count
- **WHEN** calling `colourSteps('#ff0000', '#0000ff', 5)`
- **THEN** the result has exactly 5 colours
**Verified by:** `src/colour-factories/colour-factories.test.ts::"steps produces correct count"`
---
### Requirement: Zod schema validation on public boundaries
The package SHALL validate `ColourInput` via Zod schemas at public entrypoints. Invalid input SHALL throw `COLOUR_CODES.INVALID_INPUT`.
#### Scenario: Zod rejects non-finite channel values
- **WHEN** constructing `new Colour({ r: NaN, g: 0, b: 0 })`
- **THEN** it throws `ColourError` with code `COLOUR.INVALID_INPUT`
**Verified by:** `src/parser/parser.test.ts::"rejects NaN channel values"`
---
### Requirement: Subpath exports and sideEffects false
The package SHALL declare `sideEffects: false` and provide subpath exports for `./types/colour-types`, `./errors/errors`, `./error-codes/error-codes`, `./parser/parser`, `./conversions/conversions`, `./colour/colour`, and `./colour-factories/colour-factories`.
#### Scenario: Tree-shake test passes
- **WHEN** bundling with only `parseColour` imported
- **THEN** `Colour` class code is not included in the output
**Verified by:** `tests/tree-shake.test.ts::"tree-shakes unused exports"`
<!-- markdownlint-enable MD024 -->