No description
  • TypeScript 82.4%
  • CSS 12.1%
  • HTML 5.5%
Find a file
Joshue Abance dd7ca62af1
All checks were successful
CI & Deploy / ci (push) Successful in 16s
CI & Deploy / deployment (push) Successful in 39s
fix: keep default and random colors exact to their hex value
Default swatches were stored as rounded channel values (e.g. 0.231/0.51/0.965)
that display as #3b82f6 but aren't exactly that color, so OKLCH and other
formats showed slightly different numbers than oklch.com. Define defaults
as hex and snap random colors to 8-bit channels so they round-trip exactly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 10:51:05 +08:00
.forgejo/workflows feat: setup ci auto deploy 2026-10-12 10:36:58 +08:00
public init working app 2026-10-12 10:28:53 +08:00
src fix: keep default and random colors exact to their hex value 2026-10-12 10:51:05 +08:00
.gitignore feat: setup ci auto deploy 2026-10-12 10:36:58 +08:00
.oxlintrc.json init working app 2026-10-12 10:28:53 +08:00
bun.lock init working app 2026-10-12 10:28:53 +08:00
index.html init working app 2026-10-12 10:28:53 +08:00
package.json feat: setup ci auto deploy 2026-10-12 10:36:58 +08:00
README.md feat: setup ci auto deploy 2026-10-12 10:36:58 +08:00
tsconfig.app.json init working app 2026-10-12 10:28:53 +08:00
tsconfig.json init working app 2026-10-12 10:28:53 +08:00
tsconfig.node.json init working app 2026-10-12 10:28:53 +08:00
vite.config.ts init working app 2026-10-12 10:28:53 +08:00

ccolors

A side-by-side color converter. Each column holds one color. Edit any format in it and every other format updates live.

Features

  • 11 formats: HEX, RGB, HSL, HSV/HSB, HWB, OKLCH, OKLab, LCH, Lab, Display P3, CMYK
  • Paste anything: the "Paste any" field accepts any supported notation
  • Gamut warnings: colors outside sRGB (e.g. from OKLCH or P3) are clipped and flagged
  • Alpha: a slider, plus alpha in every format that supports it
  • Native color picker and EyeDropper (on browsers that support the EyeDropper API)
  • Copy buttons: one per format
  • Multiple columns: add, duplicate and remove colors; they're saved to localStorage

Conversions follow CSS Color Module Level 4.

Development

bun install
bun run dev      # start the dev server
bun run build    # type-check and build to dist/
bun run preview  # serve the production build
bun run lint     # oxlint
bun run typecheck

CI / Deploy

.forgejo/workflows/ci.yml runs lint, typecheck and build on every push and pull request. When a push to main passes, it deploys to Vercel using the deploy environment. The workflow needs these secrets: VERCEL_TOKEN, VERCEL_ORG_ID and VERCEL_PROJECT_ID.

Built with React 19 (React Compiler enabled), TypeScript and Vite.

Project structure

src/
├── App.tsx               # layout: toolbar + color columns
├── main.tsx              # entry point
├── index.css             # styles
├── components/
│   ├── Toolbar.tsx
│   ├── ColorColumn.tsx   # one color: header, picker, alpha, fields
│   ├── ColumnHeader.tsx  # hex label, duplicate/remove buttons
│   ├── ColorPicker.tsx   # native picker + EyeDropper
│   ├── AlphaSlider.tsx
│   ├── ColorField.tsx    # editable, parse-as-you-type input
│   └── FormatField.tsx   # ColorField bound to a format
├── hooks/
│   ├── useSwatches.ts    # column list, persisted to localStorage
│   └── useCopy.ts        # clipboard with "copied" feedback
├── color/
│   ├── index.ts          # public API
│   ├── types.ts          # RGBA, Parsed, Format
│   ├── math.ts           # vector/matrix helpers, number formatting
│   ├── matrices.ts       # CSS Color 4 conversion matrices
│   ├── spaces.ts         # sRGB ↔ OKLab, Lab, HSL, HSV, HWB, …
│   ├── parse.ts          # tokenizer shared by the parsers
│   ├── formats.ts        # format registry (format + parse per notation)
│   └── utils.ts          # contrast text, CSS output, random colors
└── types/
    └── eyedropper.d.ts   # EyeDropper API typing

Adding a format

Add an entry to FORMATS in src/color/formats.ts with id, label, placeholder, format and parse. The UI renders a field for it automatically, and "Paste any" picks it up.