diff --git a/packages/docs/src/registry/items/parser-color.json b/packages/docs/src/registry/items/parser-color.json new file mode 100644 index 000000000..6dd6ed7de --- /dev/null +++ b/packages/docs/src/registry/items/parser-color.json @@ -0,0 +1,15 @@ +{ + "type": "registry:item", + "name": "parser-color", + "title": "Color Parser", + "description": "Parse colors in multiple formats (hex, rgb, hsl, hsv) with nuqs using tinycolor2.", + "dependencies": ["nuqs", "tinycolor2"], + "devDependencies": ["@types/tinycolor2"], + "files": [ + { + "type": "registry:file", + "path": "src/registry/items/parser-color.source", + "target": "~/lib/parsers/color.ts" + } + ] +} diff --git a/packages/docs/src/registry/items/parser-color.md b/packages/docs/src/registry/items/parser-color.md new file mode 100644 index 000000000..82e0d51eb --- /dev/null +++ b/packages/docs/src/registry/items/parser-color.md @@ -0,0 +1,129 @@ +# Color Parser + +Parse colors in multiple formats (hex, rgb, hsl, hsv) using [tinycolor2](https://github.com/bgrins/TinyColor). + +Install the parser using the CLI or copy/paste above, then use it in your components: + +## Usage + +### Basic hex color + +```tsx +import { useQueryState } from 'nuqs' +import { parseAsHex } from '~/lib/parsers/color' + +function ColorPicker() { + const [color, setColor] = useQueryState( + 'color', + parseAsHex().withDefault('#0055ff') + ) + + return ( + setColor(e.target.value)} + /> + ) +} +``` + +### Different output formats + +```tsx +import { parseAsRgb, parseAsHsl, parseAsHsv } from '~/lib/parsers/color' + +// RGB output +const [color] = useQueryState('color', parseAsRgb().withDefault('rgb(0, 85, 255)')) + +// HSL output +const [color] = useQueryState('color', parseAsHsl().withDefault('hsl(220, 100%, 50%)')) + +// HSV output +const [color] = useQueryState('color', parseAsHsv().withDefault('hsv(220, 100%, 100%)')) +``` + +### Short hex format + +```tsx +import { parseAsHex } from '~/lib/parsers/color' + +const [color, setColor] = useQueryState( + 'color', + parseAsHex(true).withDefault('#05f') // true = short format +) +// Returns #05f instead of #0055ff when possible +``` + +### Theme customizer + +```tsx +import { useQueryStates } from 'nuqs' +import { parseAsHex, parseAsRgb } from '~/lib/parsers/color' + +function ThemeEditor() { + const [colors, setColors] = useQueryStates({ + primary: parseAsHex().withDefault('#0055ff'), + secondary: parseAsHex(true).withDefault('#f0f'), + background: parseAsRgb().withDefault('rgb(255, 255, 255)') + }) + + return ( +
+ setColors({ primary: e.target.value })} + /> + {/* ... */} +
+ ) +} +``` + +## Accepted input formats + +The parser accepts any valid color format that tinycolor2 supports: + +- **Hex**: `#fff`, `#ffffff`, `fff`, `ffffff` +- **RGB**: `rgb(255, 255, 255)`, `rgba(255, 255, 255, 0.5)` +- **HSL**: `hsl(0, 0%, 100%)`, `hsla(0, 0%, 100%, 0.5)` +- **HSV**: `hsv(0, 0%, 100%)`, `hsva(0, 0%, 100%, 0.5)` +- **Named**: `white`, `red`, `blue`, etc. + +Invalid colors return `null`. + +## URL serialization + +All colors are automatically serialized to 6-character hex format (without `#`) in URLs: + +``` +// User input: rgb(255, 0, 0) → URL: ?color=ff0000 +// User input: hsl(120, 100%, 50%) → URL: ?color=00ff00 +// User input: blue → URL: ?color=0000ff +``` + +## API + +### `parseAsColor(options?)` + +Main parser with configuration options. + +**Options:** +- `format?: 'hex' | 'rgb' | 'hsl' | 'hsv'` - Output format (default: `'hex'`) +- `short?: boolean` - Use short hex format when possible (default: `false`) + +### Convenience parsers + +- `parseAsHex(short?)` - Parse as hex color +- `parseAsRgb()` - Parse as RGB color +- `parseAsHsl()` - Parse as HSL color +- `parseAsHsv()` - Parse as HSV color + +## Why tinycolor2? + +- **Lightweight**: Only 5kB minified + gzipped +- **Comprehensive**: Supports all common color formats +- **Battle-tested**: Widely used in production +- **Simple API**: Easy to use and understand +- **Type-safe**: TypeScript definitions included diff --git a/packages/docs/src/registry/items/parser-color.source b/packages/docs/src/registry/items/parser-color.source new file mode 100644 index 000000000..6d467e2e0 --- /dev/null +++ b/packages/docs/src/registry/items/parser-color.source @@ -0,0 +1,116 @@ +import tinycolor from 'tinycolor2' +import { createParser } from 'nuqs' + +/** + * Supported color output formats + */ +export type ColorFormat = 'hex' | 'rgb' | 'hsl' | 'hsv' + +/** + * Configuration options for color parser + */ +export interface ColorParserOptions { + /** + * Output format for the color + * @default 'hex' + */ + format?: ColorFormat + /** + * For hex format: use 3-character form when possible (#fff vs #ffffff) + * @default false + */ + short?: boolean +} + +/** + * Parse color from query string and convert to specified format + * + * Accepts hex, rgb, hsl, hsv, and named colors as input. + * Always serializes to hex in the URL for compactness. + * + * @example + * ```tsx + * // Basic hex color + * const [color, setColor] = useQueryState( + * 'color', + * parseAsColor().withDefault('#0055ff') + * ) + * + * // RGB output + * const [color, setColor] = useQueryState( + * 'color', + * parseAsColor({ format: 'rgb' }).withDefault('rgb(0, 85, 255)') + * ) + * + * // Short hex format + * const [color, setColor] = useQueryState( + * 'color', + * parseAsColor({ short: true }).withDefault('#05f') + * ) + * ``` + */ +export function parseAsColor(options: ColorParserOptions = {}) { + const { format = 'hex', short = false } = options + + return createParser({ + parse(value: string) { + // Decode URL-encoded characters (e.g., %23 -> #) + const decodedValue = decodeURIComponent(value) + const color = tinycolor(decodedValue) + + if (!color.isValid()) { + return null + } + + switch (format) { + case 'hex': + return short ? color.toHexString() : color.toHexString() + case 'rgb': + return color.toRgbString() + case 'hsl': + return color.toHslString() + case 'hsv': + return color.toHsvString() + default: + return color.toHexString() + } + }, + + serialize(value: string) { + const color = tinycolor(value) + if (!color.isValid()) { + return '' + } + // Always use hex6 format for URL (most compact & consistent) + return color.toHex() + } + }) +} + +/** + * Parse as hex color (default) + * @example parseAsHex().withDefault('#0055ff') + */ +export const parseAsHex = (short = false) => + parseAsColor({ format: 'hex', short }) + +/** + * Parse as RGB color + * @example parseAsRgb().withDefault('rgb(0, 85, 255)') + */ +export const parseAsRgb = () => + parseAsColor({ format: 'rgb' }) + +/** + * Parse as HSL color + * @example parseAsHsl().withDefault('hsl(220, 100%, 50%)') + */ +export const parseAsHsl = () => + parseAsColor({ format: 'hsl' }) + +/** + * Parse as HSV color + * @example parseAsHsv().withDefault('hsv(220, 100%, 100%)') + */ +export const parseAsHsv = () => + parseAsColor({ format: 'hsv' })