paletteguard

MoonBit palette accessibility guard for WCAG contrast reports and design-token checks.

accessibility
color
contrast
palette
wcag
moon add Jay7724/paletteguard@0.1.0
Download zip
Author
Version
0.1.0
License
MIT
Last updated
2 hours ago
Downloads
3
README

#PaletteGuard

CI

PaletteGuard is a MoonBit library for auditing design-system color palettes against WCAG contrast thresholds. It parses common color tokens, models palette roles, checks foreground/background pairs, suggests repair candidates, audits token documents, and exports reproducible Markdown reports that can be used in CI, documentation, or release reviews.

The package targets projects that keep color tokens in code, static assets, or documentation and need a small MoonBit-native guard before shipping UI themes. It is not a general image-processing library and does not depend on browser, Canvas, JavaScript, or external color packages.

#Why

Color regressions are easy to miss when teams change brand colors, terminal themes, dashboards, charts, or documentation styles. PaletteGuard gives MoonBit projects a reusable contrast engine rather than a one-off script: the same core functions can power tests, command examples, token importers, design-token release gates, and future file-format adapters.

#Installation

The module name in this repository is:

Jay7724/paletteguard

After publishing under your Mooncakes owner, install it with:

moon add Jay7724/paletteguard

Use the package from another MoonBit package:

import {
"Jay7724/paletteguard" @paletteguard,
}

#Minimal Example

let palette = [
@paletteguard.swatch("text", @paletteguard.rgb(18, 24, 38).unwrap(), @paletteguard.RoleText),
@paletteguard.swatch("paper", @paletteguard.rgb(255, 255, 255).unwrap(), @paletteguard.RoleBackground),
]

let report = @paletteguard.audit_palette(palette, @paletteguard.policy_aa())
println(report.to_markdown())
println(@paletteguard.contrast_matrix(palette, @paletteguard.policy_aa()))

Run the included example:

moon run cmd/main

#Local Commands

moon check moon build moon test moon run cmd/main moon publish --dry-run

For a real release:

moon login moon publish --dry-run moon publish

If you change code after a release, increment version in moon.mod before publishing again.

#Core API

  • Color: RGB color with 8-bit channels.
  • Role: palette role: text, accent, background, surface, or border.
  • Policy: AA/AAA and normal/large-text threshold selection.
  • parse_color: parses #RGB, #RRGGBB, rgb(r, g, b), and a small named color set.
  • parse_swatch: converts a named token into a role-aware swatch.
  • contrast_ratio: computes WCAG relative-luminance contrast.
  • audit_pair: checks one foreground/background pair.
  • audit_palette: checks all text/accent swatches against background/surface swatches.
  • PaletteReport::to_markdown: exports a stable Markdown report.
  • Color::to_hsl, Hsl::to_color, Color::mix, Color::lighten, Color::darken, Color::rotate_hue: MoonBit-native color transforms.
  • repair_foreground and repair_background: search bounded deterministic paths and return the nearest passing candidate found for a selected policy; if the policy is unreachable on those paths, return the strongest available candidate.
  • parse_token_document and audit_token_document: parse simple token assignments such as text.body = #111827 or surface.panel: white.
  • palette_stats, audit_ramp, and contrast_matrix: summarize palettes, validate luminance ramps, and export foreground/background matrices.
  • built_in_catalog, catalog_by_family, catalog_find, catalog_audit_family: 26 original semantic color families with 338 swatches for examples, tests, and starter palettes.

#Supported Scope

  • Hex colors: #RGB and #RRGGBB.
  • Decimal RGB functions: rgb(12, 34, 56).
  • Named colors: black, white, red, green, blue.
  • WCAG AA and AAA thresholds for normal and large text.
  • HSL conversion, hue rotation, grayscale, channel distance, perceived brightness, and temperature classification.
  • Foreground/background repair search using deterministic black/white and lightness paths.
  • Simple design-token document parsing with per-line diagnostics.
  • Built-in original palette catalog: 26 families, 338 semantic swatches.
  • Markdown report export.
  • Pure MoonBit implementation with no runtime dependencies.

#Not Supported

  • Alpha blending, gradients, ICC profiles, images, or screenshots.
  • CSS Color Level 4 syntax such as lab(), oklch(), or percentage RGB.
  • Reading files from disk directly. The library parses strings; CLI/file adapters are planned as separate boundary packages or future minor versions.

#CI

GitHub Actions is configured in .github/workflows/ci.yml and runs:

  • MoonBit toolchain installation
  • moon version --all
  • moon fmt --check
  • moon check --target all --deny-warn
  • moon build
  • moon test --deny-warn
  • moon package
  • moon run cmd/main

The workflow is intentionally warning-strict so that a passing check is useful evidence for a release review. It only requests read access to repository contents and does not publish packages automatically.

Project records are kept in docs/: see API.md, DESIGN.md, TEST_RECORD.md, RELEASE.md, AI_ASSISTANCE.md, and ACCEPTANCE_CHECKLIST.md.

#Mooncakes

  • Package name: Jay7724/paletteguard
  • Documentation URL after release: https://mooncakes.io/docs/Jay7724/paletteguard
  • Manifest URL after release: https://mooncakes.io/api/v0/manifest/Jay7724/paletteguard

The current repository metadata uses owner Jay7724. If the Mooncakes account shows a different owner during release, update the owner segment in moon.mod, this README, and docs/RELEASE.md before publishing.

#License And Third-Party Notes

PaletteGuard is licensed under MIT. The implementation is original MoonBit code. It uses the public WCAG contrast formula as a standard, but it does not copy third-party source code, images, fonts, audio, or datasets. The built-in catalog is original synthetic palette data maintained inside this repository.

#
CatalogFamilySummary

pub(all) struct CatalogFamilySummary {
family : String
total : Int
foregrounds : Int
backgrounds : Int
average_luminance : Double
minimum_recommended_contrast : Double
} derive(Eq,
Debug
)

#
CatalogSwatch

pub(all) struct CatalogSwatch {
id : String
family : String
step : Int
color : Color
role : Role
tone : CatalogTone
recommended_foreground : Color
target_background : Color
note : String
} derive(Eq,
Debug
)

#
CatalogSwatch::as_swatch

fn CatalogSwatch::as_swatch(self : CatalogSwatch) -> Swatch

#
CatalogSwatch::recommended_audit

fn CatalogSwatch::recommended_audit(self : CatalogSwatch, policy : Policy) -> PairAudit

#
CatalogTone

pub(all) enum CatalogTone {
ToneCanvas
ToneSurface
ToneDivider
ToneAccent
ToneBodyText
ToneStrongText
} derive(Eq,
Debug
)

#
CatalogTone::label

fn CatalogTone::label(self : CatalogTone) -> String

#
ChannelStats

pub(all) struct ChannelStats {
min : Int
max : Int
average : Double
} derive(Eq,
Debug
)

#
Color

pub(all) struct Color {
r : Int
g : Int
b : Int
} derive(Eq,
Debug
)

RGB color with 8-bit channels.

#
Color::blue

fn Color::blue(self : Color) -> Int

#
Color::channel_spread

fn Color::channel_spread(self : Color) -> Int

#
Color::darken

fn Color::darken(self : Color, amount : Double) -> Color

#
Color::desaturate

fn Color::desaturate(self : Color, amount : Double) -> Color

#
Color::distance_rgb

fn Color::distance_rgb(self : Color, other : Color) -> Double

#
Color::grayscale

fn Color::grayscale(self : Color) -> Color

#
Color::green

fn Color::green(self : Color) -> Int

#
Color::harmonies

fn Color::harmonies(self : Color, kind : HarmonyKind) -> Array[Color]

#
Color::ideal_text_color

fn Color::ideal_text_color(self : Color) -> Color

#
Color::invert

fn Color::invert(self : Color) -> Color

#
Color::is_dark

fn Color::is_dark(self : Color) -> Bool

#
Color::is_light

fn Color::is_light(self : Color) -> Bool

#
Color::lighten

fn Color::lighten(self : Color, amount : Double) -> Color

#
Color::mix

fn Color::mix(self : Color, other : Color, amount : Double) -> Color

#
Color::perceived_brightness

fn Color::perceived_brightness(self : Color) -> Double

#
Color::profile

fn Color::profile(self : Color) -> ColorProfile

#
Color::red

fn Color::red(self : Color) -> Int

#
Color::relative_luminance

fn Color::relative_luminance(self : Color) -> Double

#
Color::rotate_hue

fn Color::rotate_hue(self : Color, degrees : Double) -> Color

#
Color::saturate

fn Color::saturate(self : Color, amount : Double) -> Color

#
Color::set_lightness

fn Color::set_lightness(self : Color, lightness : Double) -> Color

#
Color::set_saturation

fn Color::set_saturation(self : Color, saturation : Double) -> Color

#
Color::temperature

fn Color::temperature(self : Color) -> Temperature

#
Color::to_hex

fn Color::to_hex(self : Color) -> String

#
Color::to_hsl

fn Color::to_hsl(self : Color) -> Hsl

#
Color::weighted_distance

fn Color::weighted_distance(self : Color, other : Color) -> Double

#
ColorParse

pub(all) enum ColorParse {
ColorParsed(Color)
ColorParseFailed(ParseError)
} derive(Eq,
Debug
)

#
ColorProfile

pub(all) struct ColorProfile {
color : Color
hex : String
luminance : Double
brightness : Double
saturation : Double
hue : Double
temperature : Temperature
ideal_text : Color
} derive(Eq,
Debug
)

#
DocumentAudit

pub(all) struct DocumentAudit {
tokens : TokenParseReport
palette : PaletteReport
stats : PaletteStats
} derive(Eq,
Debug
)

#
DocumentAudit::to_markdown

fn DocumentAudit::to_markdown(self : DocumentAudit) -> String

#
Grade

pub(all) enum Grade {
GradeFail
GradeLarge
GradeAA
GradeAAA
} derive(Eq,
Debug
)

#
Grade::label

fn Grade::label(self : Grade) -> String

#
HarmonyKind

pub(all) enum HarmonyKind {
HarmonyComplementary
HarmonyAnalogous
HarmonyTriadic
HarmonySplitComplementary
HarmonyMonochrome
HarmonyCustom
} derive(Eq,
Debug
)

#
HarmonyKind::label

fn HarmonyKind::label(self : HarmonyKind) -> String

#
Hsl

pub(all) struct Hsl {
h : Double
s : Double
l : Double
} derive(Eq,
Debug
)

#
Hsl::rotate

fn Hsl::rotate(self : Hsl, degrees : Double) -> Hsl

#
Hsl::to_color

fn Hsl::to_color(self : Hsl) -> Color

#
Hsl::with_lightness

fn Hsl::with_lightness(self : Hsl, lightness : Double) -> Hsl

#
Hsl::with_saturation

fn Hsl::with_saturation(self : Hsl, saturation : Double) -> Hsl

#
PairAudit

pub(all) struct PairAudit {
foreground : Swatch
background : Swatch
ratio : Double
required : Double
grade : Grade
passed : Bool
suggestion : Suggestion
} derive(Eq,
Debug
)

#
PaletteReport

pub(all) struct PaletteReport {
pairs : Array[PairAudit]
checked : Int
failures : Int
} derive(Eq,
Debug
)

#
PaletteReport::passes

fn PaletteReport::passes(self : PaletteReport) -> Bool

#
PaletteReport::to_markdown

fn PaletteReport::to_markdown(self : PaletteReport) -> String

#
PaletteStats

pub(all) struct PaletteStats {
swatches : Int
foregrounds : Int
backgrounds : Int
borders : Int
average_luminance : Double
darkest : Swatch?
lightest : Swatch?
} derive(Eq,
Debug
)

#
PaletteStats::to_markdown

fn PaletteStats::to_markdown(self : PaletteStats) -> String

#
ParseError

pub(all) enum ParseError {
EmptyInput
InvalidHexLength(Int)
InvalidHexDigit(Char)
InvalidRgbFunction(String)
ComponentOutOfRange(String, Int)
UnknownColorName(String)
} derive(Eq,
Debug
)

#
Policy

pub(all) struct Policy {
requirement : Requirement
text_kind : TextKind
} derive(Eq,
Debug
)

#
RampAudit

pub(all) struct RampAudit {
family : String
swatches : Array[Swatch]
monotonic_luminance : Bool
duplicate_hexes : Array[String]
smallest_luminance_gap : Double
} derive(Eq,
Debug
)

#
RampAudit::to_markdown

fn RampAudit::to_markdown(self : RampAudit) -> String

#
RepairCandidate

pub(all) struct RepairCandidate {
color : Color
ratio : Double
delta : Double
strategy : RepairStrategy
steps : Int
} derive(Eq,
Debug
)

#
RepairStrategy

pub(all) enum RepairStrategy {
RepairKeep
RepairUseIdealText
RepairDarkenForeground
RepairLightenForeground
RepairDarkenBackground
RepairLightenBackground
} derive(Eq,
Debug
)

#
RepairStrategy::label

fn RepairStrategy::label(self : RepairStrategy) -> String

#
Requirement

pub(all) enum Requirement {
LevelAA
LevelAAA
} derive(Eq,
Debug
)

WCAG conformance target.

#
Role

pub(all) enum Role {
RoleText
RoleAccent
RoleBackground
RoleSurface
RoleBorder
} derive(Eq,
Debug
)

A color role inside a design palette or token set.

#
Role::is_background

fn Role::is_background(self : Role) -> Bool

#
Role::is_foreground

fn Role::is_foreground(self : Role) -> Bool

#
Role::label

fn Role::label(self : Role) -> String

#
Severity

pub(all) enum Severity {
SeverityInfo
SeverityWarning
SeverityError
} derive(Eq,
Debug
)

#
Severity::label

fn Severity::label(self : Severity) -> String

#
Suggestion

pub(all) enum Suggestion {
SuggestKeep
SuggestUseBlack
SuggestUseWhite
SuggestIncreaseSeparation
} derive(Eq,
Debug
)

#
Suggestion::label

fn Suggestion::label(self : Suggestion) -> String

#
Swatch

pub(all) struct Swatch {
name : String
color : Color
role : Role
} derive(Eq,
Debug
)

#
SwatchParse

pub(all) enum SwatchParse {
SwatchParsed(Swatch)
SwatchParseFailed(String, ParseError)
} derive(Eq,
Debug
)

#
Temperature

pub(all) enum Temperature {
TemperatureCool
TemperatureNeutral
TemperatureWarm
} derive(Eq,
Debug
)

#
Temperature::label

fn Temperature::label(self : Temperature) -> String

#
TextKind

pub(all) enum TextKind {
NormalText
LargeText
} derive(Eq,
Debug
)

Text size class used by WCAG contrast thresholds.

#
TokenDiagnostic

pub(all) struct TokenDiagnostic {
line : Int
column : Int
token : String
severity : Severity
kind : TokenDiagnosticKind
message : String
} derive(Eq,
Debug
)

#
TokenDiagnosticKind

pub(all) enum TokenDiagnosticKind {
DiagnosticEmptyName
DiagnosticMissingAssignment
DiagnosticUnknownRole
DiagnosticBadColor(ParseError)
} derive(Eq,
Debug
)

#
TokenParseReport

pub(all) struct TokenParseReport {
swatches : Array[Swatch]
diagnostics : Array[TokenDiagnostic]
ignored : Int
} derive(Eq,
Debug
)

#
TokenParseReport::has_errors

fn TokenParseReport::has_errors(self : TokenParseReport) -> Bool

#
TokenParseReport::to_markdown

fn TokenParseReport::to_markdown(self : TokenParseReport) -> String

#
audit_pair

fn audit_pair(foreground : Swatch, background : Swatch, policy : Policy) -> PairAudit

#
audit_palette

fn audit_palette(swatches : Array[Swatch], policy : Policy) -> PaletteReport

#
audit_ramp

fn audit_ramp(family : String, swatches : Array[Swatch]) -> RampAudit

#
audit_token_document

fn audit_token_document(input : String, policy : Policy) -> DocumentAudit

#
built_in_catalog

fn built_in_catalog() -> Array[CatalogSwatch]

#
catalog_as_swatches

fn catalog_as_swatches(items : Array[CatalogSwatch]) -> Array[Swatch]

#
catalog_audit_family

fn catalog_audit_family(family : String, policy : Policy) -> PaletteReport

#
catalog_by_family

fn catalog_by_family(family : String) -> Array[CatalogSwatch]

#
catalog_by_role

fn catalog_by_role(role : Role) -> Array[CatalogSwatch]

#
catalog_families

fn catalog_families() -> Array[String]

#
catalog_find

fn catalog_find(id : String) -> CatalogSwatch?

#
catalog_markdown

fn catalog_markdown() -> String

#
catalog_palette_for_family

fn catalog_palette_for_family(family : String) -> Array[Swatch]

#
catalog_summary

fn catalog_summary(family : String) -> CatalogFamilySummary

#
channel_stats

fn channel_stats(swatches : Array[Swatch]) -> (ChannelStats, ChannelStats, ChannelStats)

#
contrast_matrix

fn contrast_matrix(swatches : Array[Swatch], policy : Policy) -> String

#
contrast_ratio

fn contrast_ratio(a : Color, b : Color) -> Double

#
demo_palette

fn demo_palette() -> Array[Swatch]

#
demo_report

fn demo_report() -> String

#
grade_ratio

fn grade_ratio(ratio : Double) -> Grade

#
minimum_ratio

fn minimum_ratio(policy : Policy) -> Double

#
palette_stats

fn palette_stats(swatches : Array[Swatch]) -> PaletteStats

#
parse_color

fn parse_color(input : String) -> ColorParse

#
parse_role_token

fn parse_role_token(token : String) -> Role?

#
parse_swatch

fn parse_swatch(name : String, color_token : String, role : Role) -> SwatchParse

#
parse_token_document

fn parse_token_document(input : String) -> TokenParseReport

#
policy_aa

fn policy_aa() -> Policy

#
policy_aaa

fn policy_aaa() -> Policy

#
policy_large_aa

fn policy_large_aa() -> Policy

#
policy_large_aaa

fn policy_large_aaa() -> Policy

#
readable_foreground

fn readable_foreground(background : Color, policy : Policy) -> RepairCandidate

#
repair_background

fn repair_background(foreground : Color, background : Color, policy : Policy) -> RepairCandidate

#
repair_foreground

fn repair_foreground(foreground : Color, background : Color, policy : Policy) -> RepairCandidate

#
rgb

fn rgb(r : Int, g : Int, b : Int) -> Color?

#
swatch

fn swatch(name : String, color : Color, role : Role) -> Swatch

#
swatch_profile

fn swatch_profile(s : Swatch) -> ColorProfile

Powered by MoonBit

Site sourceReport issuePackagesBuild queueSkillsStatistics

© 2026 mooncakes.io