Huevora
A production-grade color engine for Dart. Derive complete design-system palettes from a single hex color, generate Material-style tonal palettes, validate contrast via APCA and WCAG 2.x, and export to JSON or plain text.
Features
| Feature | Description |
|---|---|
| Bidirectional color conversion | HEX ↔ OKLCH ↔ RGB with prism-backed precision |
| Branded palette derivation | 9 standard roles (primary, secondary, tertiary, neutral, neutralVariant, success, error, warning, info) derived perceptually in OKLCH space |
| Tonal palette generation | Material 3-style tone steps via material_color_utilities, with asymmetric neutral step arrays |
| Contrast validation | APCA 0.0.98G + WCAG 2.x simultaneous checking with tone suggestions |
| Export system | JSON (structured) and TXT (token-style, Figma-friendly) with configurable inclusion flags |
| Gamut-safe by design | Every derived color is verified in-gamut via round-trip OKLCH→RGB8→OKLCH validation |
| Null-safe, sealed errors | Exhaustive exception hierarchy; no stringly-typed failures |
Quickstart
import 'package:huevora/huevora.dart';
void main() {
final engine = ColorEngine();
// Derive a complete branded palette from one hex color.
final palette = engine.deriveCorePalette('#4A90E2');
// Generate tonal palettes for every role.
final tonals = engine.generateTonalPalettes(palette);
// Check contrast.
final contrast = ContrastEngine().check(
foreground: palette.primary,
background: engine.fromHex('#FFFFFF'),
);
print('APCA Lc: ${contrast.apcaLc}, WCAG: ${contrast.wcagRatio}:1');
// Export to JSON.
final json = ExportEngine().toJson(palette, tonals);
print(json);
}
Installation
dependencies:
huevora: ^1.0.2+1
dart pub add huevora
API Overview
ColorEngine
final engine = ColorEngine();
// Derive from primary hex
final palette = engine.deriveCorePalette('#4A90E2', DerivationConfig(
semanticBrandingWeight: 0.25,
customColors: [(name: 'accent', hex: '#FF6B35')],
));
// Or validate a fully-specified palette
final validated = engine.validateCorePalette(CorePaletteInput(...));
// Tonal generation
final tonals = engine.generateTonalPalettes(palette);
ContrastEngine
final result = ContrastEngine().check(
foreground: palette.primary,
background: engine.fromHex('#FFFFFF'),
tonalResult: tonals,
fgRole: ColorRole.primary,
);
print(result.advice);
print(result.suggestedFgTones); // tone alternatives from the palette
ExportEngine
final export = ExportEngine();
// JSON with full metadata
final json = export.toJson(palette, tonals);
// Hex-only, core-only
final minimal = export.toJson(
palette,
tonals,
ExportConfig.hexOnly(),
);
// Plain text tokens
final text = export.toText(palette, tonals);
// Write to disk
await export.writeToFile(json, './palette.json');
Color Derivation Logic
| Role | Derivation |
|---|---|
primary |
Input seed color |
secondary |
Analogous (+30° hue offset, 65% chroma) |
tertiary |
Complementary (+180° hue, 70% chroma) |
neutral |
Primary hue, chroma clamped to 0.018, 0.10 |
neutralVariant |
Primary hue, chroma clamped to 0.045, 0.10 |
success |
Base hue 145°, pulled 25% toward primary hue |
error |
Base hue 25°, pulled 25% toward primary hue |
warning |
Base hue 75°, pulled 25% toward primary hue |
info |
Base hue 240°, pulled 25% toward primary hue |
Tone Step Arrays
| Role type | Steps |
|---|---|
| Standard (chromatic) | 0, 5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 95, 98, 99, 100 |
| Neutral | 0, 4, 5, 6, 10, 12, 15, 17, 20, 22, 24, 25, 30, 35, 40, 50, 60, 70, 80, 87, 90, 92, 94, 95, 96, 98, 99, 100 |
Neutral roles use denser steps at dark and light extremes for precise elevation overlay and surface tint control.
Contrast Standards
| Standard | Metric | Thresholds |
|---|---|---|
| APCA | Signed Lc (lightness contrast) | ≥90 fluent text, ≥75 body text, ≥60 large text, ≥45 UI components |
| WCAG 2.x | Contrast ratio | ≥7.0 AAA, ≥4.5 AA, ≥3.0 AA Large, <3.0 fail |
APCA is polarity-sensitive (dark-on-light ≠ light-on-dark in |Lc|). WCAG 2.x is order-independent.
Error Handling
All failures are typed via a sealed HuevoraException hierarchy:
try {
final color = engine.fromHex('#ZZZZZZ');
} on InvalidHexException catch (e) {
print('Bad input: ${e.input}');
}
try {
GamutGuard.assertInSrgb(color);
} on OutOfGamutException catch (e) {
print('Use ${e.clampedHex} instead of ${e.sourceHex}');
}
Dependencies
| Package | Version | Purpose |
|---|---|---|
prism |
^2.1.0 | OKLCH/OKLab/RGB8 conversion, gamut clipping |
material_color_utilities |
^0.13.0 | HCT tonal palette generation |
License
MIT
Contributing
Issues and PRs welcome. All changes must pass the full test suite (dart test) and maintain backward compatibility of public API types.
Libraries
- huevora
- Huevora — Color Engine for Dart.