@cobalt-ui/core
Advanced tools
Comparing version 1.8.1 to 1.9.0
# @cobalt-ui/core | ||
## 1.9.0 | ||
### Minor Changes | ||
- [#228](https://github.com/drwpow/cobalt-ui/pull/228) [`1e12d04fb24eebc1df152017935dd65a2c6d7618`](https://github.com/drwpow/cobalt-ui/commit/1e12d04fb24eebc1df152017935dd65a2c6d7618) Thanks [@drwpow](https://github.com/drwpow)! - Add gamut clipping for color tokens | ||
### Patch Changes | ||
- [#228](https://github.com/drwpow/cobalt-ui/pull/228) [`1e12d04fb24eebc1df152017935dd65a2c6d7618`](https://github.com/drwpow/cobalt-ui/commit/1e12d04fb24eebc1df152017935dd65a2c6d7618) Thanks [@drwpow](https://github.com/drwpow)! - Improve Tokens Studio inline aliasing | ||
- [#228](https://github.com/drwpow/cobalt-ui/pull/228) [`1e12d04fb24eebc1df152017935dd65a2c6d7618`](https://github.com/drwpow/cobalt-ui/commit/1e12d04fb24eebc1df152017935dd65a2c6d7618) Thanks [@drwpow](https://github.com/drwpow)! - Make parse options optional for easier use | ||
## 1.8.1 | ||
@@ -4,0 +16,0 @@ |
@@ -20,7 +20,7 @@ import type { ParsedToken } from '../token.js'; | ||
/** Configure transformations for color tokens */ | ||
color: ParseColorOptions; | ||
color?: ParseColorOptions; | ||
figma?: FigmaParseOptions; | ||
/** Configure plugin lint rules (if any) */ | ||
lint: { | ||
rules: Record<string, LintRule>; | ||
lint?: { | ||
rules?: Record<string, LintRule>; | ||
}; | ||
@@ -27,0 +27,0 @@ } |
@@ -6,3 +6,4 @@ /** | ||
*/ | ||
import { parseAlias } from '@cobalt-ui/utils'; | ||
import { isAlias, parseAlias } from '@cobalt-ui/utils'; | ||
import { parse as culoriParse, rgb } from 'culori'; | ||
// I’m not sure this is comprehensive at all but better than nothing | ||
@@ -216,5 +217,56 @@ const FONT_WEIGHTS = { | ||
else { | ||
let value = v.value; | ||
// resolve inline aliases (e.g. `rgba({color.black}, 0.5)`) | ||
if (value.includes('{') && !v.value.startsWith('{')) { | ||
value = resolveAlias(value, path); | ||
if (!value) { | ||
errors.push(`Could not resolve "${v.value}"`); | ||
continue; | ||
} | ||
// note: we did some work earlier to help resolve the aliases, but | ||
// we need to REPLACE them in this scenario so we must do a 2nd pass | ||
const matches = value.match(ALIAS_RE); | ||
for (const match of matches ?? []) { | ||
let currentAlias = parseAlias(match).id; | ||
let resolvedValue; | ||
const aliasHistory = new Set([currentAlias]); | ||
while (!resolvedValue) { | ||
const aliasNode = get(rawTokens, currentAlias.split('.')); | ||
// does this resolve to a $value? | ||
if (aliasNode && aliasNode.value) { | ||
// is this another alias? | ||
if (isAlias(aliasNode.value)) { | ||
currentAlias = parseAlias(aliasNode.value).id; | ||
if (aliasHistory.has(currentAlias)) { | ||
errors.push(`Couldn’t resolve circular alias "${v.value}"`); | ||
break; | ||
} | ||
aliasHistory.add(currentAlias); | ||
continue; | ||
} | ||
resolvedValue = aliasNode.value; | ||
} | ||
break; | ||
} | ||
if (resolvedValue) { | ||
value = value.replace(match, resolvedValue); | ||
} | ||
} | ||
if (!culoriParse(value)) { | ||
// fix `rgba(#000000, 0.3)` scenario specifically (common Tokens Studio version) | ||
// but throw err otherwise | ||
if (value.startsWith('rgb') && value.includes('#')) { | ||
const hexValue = value.match(/#[abcdef0-9]+/i); | ||
if (hexValue && hexValue[0]) { | ||
const rgbVal = rgb(hexValue[0]); | ||
if (rgbVal) { | ||
value = value.replace(hexValue[0], `${rgbVal.r * 100}%, ${rgbVal.g * 100}%, ${rgbVal.b * 100}%`); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
addToken({ | ||
$type: 'color', | ||
$value: v.value, | ||
$value: value, | ||
$description: v.description, | ||
@@ -221,0 +273,0 @@ }, tokenPath); |
import type { ParsedBorderToken } from '../../token.js'; | ||
import type { ParseColorOptions } from './color.js'; | ||
export interface ParseBorderOptions { | ||
color: ParseColorOptions; | ||
color?: ParseColorOptions; | ||
} | ||
@@ -6,0 +6,0 @@ /** |
import type { ParsedColorToken } from '../../token.js'; | ||
export interface ParseColorOptions { | ||
/** Convert color to sRGB hex? (default: true) */ | ||
/** Convert color to 8-bit sRGB hexadecimal? (default: false) */ | ||
convertToHex?: boolean; | ||
/** Confine colors to gamut? sRGB is smaller but widely-supported; P3 supports more colors but not all users (default: `undefined`) */ | ||
gamut?: 'srgb' | 'p3' | undefined; | ||
} | ||
@@ -15,2 +17,2 @@ /** | ||
*/ | ||
export declare function normalizeColorValue(value: unknown, options: ParseColorOptions): ParsedColorToken['$value']; | ||
export declare function normalizeColorValue(rawValue: unknown, options?: ParseColorOptions): ParsedColorToken['$value']; |
@@ -1,2 +0,2 @@ | ||
import { formatHex, formatHex8, parse } from 'culori'; | ||
import { clampChroma, formatHex, formatHex8, parse, formatCss } from 'culori'; | ||
/** | ||
@@ -11,18 +11,30 @@ * 8.1 Color | ||
*/ | ||
export function normalizeColorValue(value, options) { | ||
if (!value) { | ||
export function normalizeColorValue(rawValue, options) { | ||
if (!rawValue) { | ||
throw new Error('missing value'); | ||
} | ||
if (typeof value === 'string') { | ||
if (options.convertToHex === true) { | ||
const parsed = parse(value); | ||
if (!parsed) { | ||
throw new Error(`invalid color "${value}"`); | ||
} | ||
return typeof parsed.alpha === 'number' && parsed.alpha < 1 ? formatHex8(parsed) : formatHex(parsed); | ||
if (typeof rawValue === 'string') { | ||
const parsed = parse(rawValue); | ||
if (!parsed) { | ||
throw new Error(`invalid color "${rawValue}"`); | ||
} | ||
return value; | ||
let value = parsed; | ||
let valueEdited = false; // keep track of this to reduce rounding errors | ||
// clamp to sRGB if we’re converting to hex, too! | ||
if (options?.gamut === 'srgb' || options?.convertToHex === true) { | ||
value = clampChroma(parsed, parsed.mode, 'rgb'); | ||
valueEdited = true; | ||
} | ||
else if (options?.gamut === 'p3') { | ||
value = clampChroma(parsed, parsed.mode, 'p3'); | ||
valueEdited = true; | ||
} | ||
// TODO: in 2.x, only convert to hex if no color loss (e.g. don’t downgrade a 12-bit color `rgb()` to 8-bit hex) | ||
if (options?.convertToHex === true) { | ||
return typeof value.alpha === 'number' && value.alpha < 1 ? formatHex8(value) : formatHex(value); | ||
} | ||
return valueEdited ? formatCss(value) : rawValue; // return the original value if we didn’t modify it; we may introduce nondeterministic rounding errors (the classic JS 0.3333… nonsense, etc.) | ||
} | ||
throw new Error(`expected string, received ${typeof value}`); | ||
throw new Error(`expected string, received ${typeof rawValue}`); | ||
} | ||
//# sourceMappingURL=color.js.map |
import type { ParsedGradientToken } from '../../token.js'; | ||
import type { ParseColorOptions } from './color.js'; | ||
export interface ParseGradientOptions { | ||
color: ParseColorOptions; | ||
color?: ParseColorOptions; | ||
} | ||
@@ -6,0 +6,0 @@ /** |
import type { ParsedShadowToken } from '../../token.js'; | ||
import type { ParseColorOptions } from './color.js'; | ||
export interface ParseShadowOptions { | ||
color: ParseColorOptions; | ||
color?: ParseColorOptions; | ||
} | ||
@@ -6,0 +6,0 @@ /** |
{ | ||
"name": "@cobalt-ui/core", | ||
"description": "Parser/validator for the Design Tokens Community Group (DTCG) standard.", | ||
"version": "1.8.1", | ||
"version": "1.9.0", | ||
"author": { | ||
@@ -6,0 +6,0 @@ "name": "Drew Powers", |
@@ -42,7 +42,7 @@ import { cloneDeep, FG_YELLOW, getAliasID, invalidTokenIDError, isAlias, RESET } from '@cobalt-ui/utils'; | ||
/** Configure transformations for color tokens */ | ||
color: ParseColorOptions; | ||
color?: ParseColorOptions; | ||
figma?: FigmaParseOptions; | ||
/** Configure plugin lint rules (if any) */ | ||
lint: { | ||
rules: Record<string, LintRule>; | ||
lint?: { | ||
rules?: Record<string, LintRule>; | ||
}; | ||
@@ -49,0 +49,0 @@ } |
@@ -6,3 +6,4 @@ /** | ||
*/ | ||
import { parseAlias } from '@cobalt-ui/utils'; | ||
import { isAlias, parseAlias } from '@cobalt-ui/utils'; | ||
import { parse as culoriParse, rgb } from 'culori'; | ||
import type { GradientStop, Group, Token } from '../token.js'; | ||
@@ -288,5 +289,8 @@ | ||
let order = [values[0], values[1], values[0], values[1]] as [string, string, string, string]; // TL, BR | ||
if (values.length === 3) | ||
{order = [values[0], values[1], values[2], values[1]] as [string, string, string, string];} // TL, TR/BL, BR | ||
else if (values.length === 4) {order = [values[0], values[1], values[2], values[3]] as [string, string, string, string];} // TL, TR, BR, BL | ||
if (values.length === 3) { | ||
order = [values[0], values[1], values[2], values[1]] as [string, string, string, string]; | ||
} // TL, TR/BL, BR | ||
else if (values.length === 4) { | ||
order = [values[0], values[1], values[2], values[3]] as [string, string, string, string]; | ||
} // TL, TR, BR, BL | ||
addToken({ $type: 'dimension', $value: order[0], $description: v.description }, [...path, `${k}TopLeft`]); | ||
@@ -370,6 +374,61 @@ addToken({ $type: 'dimension', $value: order[1], $description: v.description }, [...path, `${k}TopRight`]); | ||
} else { | ||
let value: string | undefined = v.value; | ||
// resolve inline aliases (e.g. `rgba({color.black}, 0.5)`) | ||
if (value.includes('{') && !v.value.startsWith('{')) { | ||
value = resolveAlias(value, path); | ||
if (!value) { | ||
errors.push(`Could not resolve "${v.value}"`); | ||
continue; | ||
} | ||
// note: we did some work earlier to help resolve the aliases, but | ||
// we need to REPLACE them in this scenario so we must do a 2nd pass | ||
const matches = value.match(ALIAS_RE); | ||
for (const match of matches ?? []) { | ||
let currentAlias = parseAlias(match).id; | ||
let resolvedValue: string | undefined; | ||
const aliasHistory = new Set<string>([currentAlias]); | ||
while (!resolvedValue) { | ||
const aliasNode: any = get(rawTokens, currentAlias.split('.')); | ||
// does this resolve to a $value? | ||
if (aliasNode && aliasNode.value) { | ||
// is this another alias? | ||
if (isAlias(aliasNode.value)) { | ||
currentAlias = parseAlias(aliasNode.value).id; | ||
if (aliasHistory.has(currentAlias)) { | ||
errors.push(`Couldn’t resolve circular alias "${v.value}"`); | ||
break; | ||
} | ||
aliasHistory.add(currentAlias); | ||
continue; | ||
} | ||
resolvedValue = aliasNode.value; | ||
} | ||
break; | ||
} | ||
if (resolvedValue) { | ||
value = value.replace(match, resolvedValue); | ||
} | ||
} | ||
if (!culoriParse(value)) { | ||
// fix `rgba(#000000, 0.3)` scenario specifically (common Tokens Studio version) | ||
// but throw err otherwise | ||
if (value.startsWith('rgb') && value.includes('#')) { | ||
const hexValue = value.match(/#[abcdef0-9]+/i); | ||
if (hexValue && hexValue[0]) { | ||
const rgbVal = rgb(hexValue[0]); | ||
if (rgbVal) { | ||
value = value.replace(hexValue[0], `${rgbVal.r * 100}%, ${rgbVal.g * 100}%, ${rgbVal.b * 100}%`); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
addToken( | ||
{ | ||
$type: 'color', | ||
$value: v.value, | ||
$value: value, | ||
$description: v.description, | ||
@@ -447,5 +506,8 @@ }, | ||
let order: [string, string, string, string] = [values[0], values[1], values[0], values[1]] as [string, string, string, string]; // TB, RL | ||
if (values.length === 3) | ||
{order = [values[0], values[1], values[2], values[1]] as [string, string, string, string];} // T, RL, B | ||
else if (values.length === 4) {order = [values[0], values[1], values[2], values[3]] as [string, string, string, string];} // T, R, B, L | ||
if (values.length === 3) { | ||
order = [values[0], values[1], values[2], values[1]] as [string, string, string, string]; | ||
} // T, RL, B | ||
else if (values.length === 4) { | ||
order = [values[0], values[1], values[2], values[3]] as [string, string, string, string]; | ||
} // T, R, B, L | ||
addToken({ $type: 'dimension', $value: order[0], $description: v.description }, [...path, `${k}Top`]); | ||
@@ -452,0 +514,0 @@ addToken({ $type: 'dimension', $value: order[1], $description: v.description }, [...path, `${k}Right`]); |
@@ -9,3 +9,3 @@ import type { BorderToken, ParsedBorderToken } from '../../token.js'; | ||
export interface ParseBorderOptions { | ||
color: ParseColorOptions; | ||
color?: ParseColorOptions; | ||
} | ||
@@ -12,0 +12,0 @@ |
@@ -1,7 +0,9 @@ | ||
import { formatHex, formatHex8, parse } from 'culori'; | ||
import { type Color, clampChroma, formatHex, formatHex8, parse, formatCss } from 'culori'; | ||
import type { ParsedColorToken } from '../../token.js'; | ||
export interface ParseColorOptions { | ||
/** Convert color to sRGB hex? (default: true) */ | ||
/** Convert color to 8-bit sRGB hexadecimal? (default: false) */ | ||
convertToHex?: boolean; | ||
/** Confine colors to gamut? sRGB is smaller but widely-supported; P3 supports more colors but not all users (default: `undefined`) */ | ||
gamut?: 'srgb' | 'p3' | undefined; | ||
} | ||
@@ -18,17 +20,32 @@ | ||
*/ | ||
export function normalizeColorValue(value: unknown, options: ParseColorOptions): ParsedColorToken['$value'] { | ||
if (!value) { | ||
export function normalizeColorValue(rawValue: unknown, options?: ParseColorOptions): ParsedColorToken['$value'] { | ||
if (!rawValue) { | ||
throw new Error('missing value'); | ||
} | ||
if (typeof value === 'string') { | ||
if (options.convertToHex === true) { | ||
const parsed = parse(value); | ||
if (!parsed) { | ||
throw new Error(`invalid color "${value}"`); | ||
} | ||
return typeof parsed.alpha === 'number' && parsed.alpha < 1 ? formatHex8(parsed) : formatHex(parsed); | ||
if (typeof rawValue === 'string') { | ||
const parsed = parse(rawValue); | ||
if (!parsed) { | ||
throw new Error(`invalid color "${rawValue}"`); | ||
} | ||
return value; | ||
let value = parsed as Color; | ||
let valueEdited = false; // keep track of this to reduce rounding errors | ||
// clamp to sRGB if we’re converting to hex, too! | ||
if (options?.gamut === 'srgb' || options?.convertToHex === true) { | ||
value = clampChroma(parsed, parsed.mode, 'rgb'); | ||
valueEdited = true; | ||
} else if (options?.gamut === 'p3') { | ||
value = clampChroma(parsed, parsed.mode, 'p3'); | ||
valueEdited = true; | ||
} | ||
// TODO: in 2.x, only convert to hex if no color loss (e.g. don’t downgrade a 12-bit color `rgb()` to 8-bit hex) | ||
if (options?.convertToHex === true) { | ||
return typeof value.alpha === 'number' && value.alpha < 1 ? formatHex8(value) : formatHex(value); | ||
} | ||
return valueEdited ? formatCss(value) : rawValue; // return the original value if we didn’t modify it; we may introduce nondeterministic rounding errors (the classic JS 0.3333… nonsense, etc.) | ||
} | ||
throw new Error(`expected string, received ${typeof value}`); | ||
throw new Error(`expected string, received ${typeof rawValue}`); | ||
} |
@@ -6,3 +6,3 @@ import type { GradientStop, ParsedGradientToken } from '../../token.js'; | ||
export interface ParseGradientOptions { | ||
color: ParseColorOptions; | ||
color?: ParseColorOptions; | ||
} | ||
@@ -9,0 +9,0 @@ |
@@ -7,3 +7,3 @@ import type { ParsedShadowToken } from '../../token.js'; | ||
export interface ParseShadowOptions { | ||
color: ParseColorOptions; | ||
color?: ParseColorOptions; | ||
} | ||
@@ -10,0 +10,0 @@ |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
702933
5329