Skip to content

Commit 7f2e5cc

Browse files
authored
feat: implement colors as color tokens (#3)
* feat: implement colors as color tokens * fix "none" type
1 parent bbc4f53 commit 7f2e5cc

File tree

8 files changed

+146
-34
lines changed

8 files changed

+146
-34
lines changed

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"dependencies": {
3737
"@projectwallace/css-analyzer": "^7.5.0",
3838
"color-sorter": "^7.0.0",
39+
"colorjs.io": "^0.6.0-alpha.1",
3940
"css-time-sort": "^3.0.0",
4041
"css-tree": "^3.1.0"
4142
},
@@ -45,4 +46,4 @@
4546
"vite-plugin-dts": "^4.5.4",
4647
"vitest": "^3.2.4"
4748
}
48-
}
49+
}

src/colors.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { KeywordSet } from './keyword-set.js'
2+
import { EXTENSION_AUTHORED_AS, type ColorToken, type ColorComponent } from './types.js'
3+
import Color from 'colorjs.io'
24

35
export const named_colors = new KeywordSet([
46
// CSS Named Colors
@@ -191,4 +193,23 @@ export const color_functions = new KeywordSet([
191193
'lch',
192194
'lab',
193195
'oklab',
194-
])
196+
])
197+
198+
export function color_to_token(color: string): ColorToken {
199+
let parsed_color = new Color(color)
200+
return {
201+
$type: 'color',
202+
$value: {
203+
colorSpace: parsed_color.space.id,
204+
components: [
205+
parsed_color.coords[0] ?? 'none',
206+
parsed_color.coords[1] ?? 'none',
207+
parsed_color.coords[2] ?? 'none',
208+
],
209+
alpha: parsed_color.alpha.valueOf(),
210+
},
211+
$extensions: {
212+
[EXTENSION_AUTHORED_AS]: color
213+
}
214+
}
215+
}

src/destructure-box-shadow.test.ts

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { test, expect, describe } from 'vitest'
22
import { destructure_box_shadow } from './destructure-box-shadow.js'
3+
import { ColorToken, EXTENSION_AUTHORED_AS } from './types.js'
4+
import { color_to_token } from './colors.js'
35

46
function create_px_length(value: number): { value: number, unit: string, $type: 'dimension' } {
57
return {
@@ -9,6 +11,20 @@ function create_px_length(value: number): { value: number, unit: string, $type:
911
}
1012
}
1113

14+
function create_color_token(): ColorToken {
15+
return {
16+
$type: 'color',
17+
$value: {
18+
colorSpace: 'srgb',
19+
components: [0, 0, 0],
20+
alpha: 1,
21+
},
22+
$extensions: {
23+
[EXTENSION_AUTHORED_AS]: '#000'
24+
}
25+
}
26+
}
27+
1228
test('handles invalid input', () => {
1329
expect.soft(destructure_box_shadow('')).toBeNull()
1430
expect.soft(destructure_box_shadow('foo')).toBeNull()
@@ -19,7 +35,7 @@ test('handles invalid input', () => {
1935
test('0px 0px 0px 0px #000', () => {
2036
expect(destructure_box_shadow('0px 0px 0px 0px #000')).toEqual([
2137
{
22-
color: '#000',
38+
color: create_color_token(),
2339
offsetX: create_px_length(0),
2440
offsetY: create_px_length(0),
2541
blur: create_px_length(0),
@@ -32,7 +48,7 @@ test('0px 0px 0px 0px #000', () => {
3248
test('adds units when omitted: 0 0 0 0 #000', () => {
3349
expect(destructure_box_shadow('0 0 0 0 #000')).toEqual([
3450
{
35-
color: '#000',
51+
color: create_color_token(),
3652
offsetX: create_px_length(0),
3753
offsetY: create_px_length(0),
3854
blur: create_px_length(0),
@@ -45,7 +61,7 @@ test('adds units when omitted: 0 0 0 0 #000', () => {
4561
test('offsetX and offsetY: 2px 4px #000', () => {
4662
expect(destructure_box_shadow('2px 4px #000')).toEqual([
4763
{
48-
color: '#000',
64+
color: create_color_token(),
4965
offsetX: create_px_length(2),
5066
offsetY: create_px_length(4),
5167
blur: create_px_length(0),
@@ -58,7 +74,7 @@ test('offsetX and offsetY: 2px 4px #000', () => {
5874
test('offsetX, offsetY and blur: 2px 4px 6px #000', () => {
5975
expect(destructure_box_shadow('2px 4px 6px #000')).toEqual([
6076
{
61-
color: '#000',
77+
color: create_color_token(),
6278
offsetX: create_px_length(2),
6379
offsetY: create_px_length(4),
6480
blur: create_px_length(6),
@@ -71,7 +87,7 @@ test('offsetX, offsetY and blur: 2px 4px 6px #000', () => {
7187
test('offsetX, offsetY, blur and spread: 2px 4px 6px 8px #000', () => {
7288
expect(destructure_box_shadow('2px 4px 6px 8px #000')).toEqual([
7389
{
74-
color: '#000',
90+
color: create_color_token(),
7591
offsetX: create_px_length(2),
7692
offsetY: create_px_length(4),
7793
blur: create_px_length(6),
@@ -84,7 +100,7 @@ test('offsetX, offsetY, blur and spread: 2px 4px 6px 8px #000', () => {
84100
test('inset: 2px 4px 6px 8px #000 inset', () => {
85101
expect(destructure_box_shadow('2px 4px 6px 8px #000 inset')).toEqual([
86102
{
87-
color: '#000',
103+
color: create_color_token(),
88104
offsetX: create_px_length(2),
89105
offsetY: create_px_length(4),
90106
blur: create_px_length(6),
@@ -97,7 +113,7 @@ test('inset: 2px 4px 6px 8px #000 inset', () => {
97113
test('INSET: 2px 4px 6px 8px #000 inset', () => {
98114
expect(destructure_box_shadow('2px 4px 6px 8px #000 INSET')).toEqual([
99115
{
100-
color: '#000',
116+
color: create_color_token(),
101117
offsetX: create_px_length(2),
102118
offsetY: create_px_length(4),
103119
blur: create_px_length(6),
@@ -110,7 +126,7 @@ test('INSET: 2px 4px 6px 8px #000 inset', () => {
110126
test('color in a different order: #000 2px 4px 6px 8px', () => {
111127
expect(destructure_box_shadow('#000 2px 4px 6px 8px')).toEqual([
112128
{
113-
color: '#000',
129+
color: create_color_token(),
114130
offsetX: create_px_length(2),
115131
offsetY: create_px_length(4),
116132
blur: create_px_length(6),
@@ -123,15 +139,15 @@ test('color in a different order: #000 2px 4px 6px 8px', () => {
123139
test('multiple shadows', () => {
124140
expect(destructure_box_shadow('2px 4px 6px 8px #000, 0 0 0 0 #fff inset')).toEqual([
125141
{
126-
color: '#000',
142+
color: create_color_token(),
127143
offsetX: create_px_length(2),
128144
offsetY: create_px_length(4),
129145
blur: create_px_length(6),
130146
spread: create_px_length(8),
131147
inset: false
132148
},
133149
{
134-
color: '#fff',
150+
color: color_to_token('#fff'),
135151
offsetX: create_px_length(0),
136152
offsetY: create_px_length(0),
137153
blur: create_px_length(0),
@@ -154,7 +170,7 @@ describe('color formats', () => {
154170
test(`1px ${color}`, () => {
155171
expect(destructure_box_shadow(`1px ${color}`)).toEqual([
156172
{
157-
color: color,
173+
color: color_to_token(color),
158174
offsetX: create_px_length(1),
159175
offsetY: create_px_length(0),
160176
blur: create_px_length(0),

src/destructure-box-shadow.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { parse, type CssNode, type Value } from 'css-tree'
2-
import { named_colors, system_colors, color_functions } from './colors'
2+
import { named_colors, system_colors, color_functions, color_to_token } from './colors.js'
3+
import type { ColorToken } from './types.js'
34

45
type CssLength = {
56
value: number
67
unit: string
78
}
89

910
export type DestructuredShadow = {
10-
color: string | undefined
11+
color: ColorToken | undefined
1112
offsetX: CssLength | undefined
1213
offsetY: CssLength | undefined
1314
blur: CssLength | undefined
@@ -51,7 +52,7 @@ export function destructure_box_shadow(value: string): null | DestructuredShadow
5152
if (node.name.toLowerCase() === 'inset') {
5253
current_shadow.inset = true
5354
} else if (named_colors.has(node.name) || system_colors.has(node.name)) {
54-
current_shadow.color = node.name
55+
current_shadow.color = color_to_token(node.name)
5556
}
5657
}
5758
else if (node.type === 'Dimension' || (node.type === 'Number' && node.value === '0')) {
@@ -77,13 +78,13 @@ export function destructure_box_shadow(value: string): null | DestructuredShadow
7778
}
7879
else if (node.type === 'Function') {
7980
if (color_functions.has(node.name)) {
80-
current_shadow.color = generate(node)
81+
current_shadow.color = color_to_token(generate(node))
8182
} else if (node.name.toLowerCase() === 'var' && !current_shadow.color) {
82-
current_shadow.color = generate(node)
83+
current_shadow.color = color_to_token(generate(node))
8384
}
8485
}
8586
else if (node.type === 'Hash') {
86-
current_shadow.color = generate(node)
87+
current_shadow.color = color_to_token(generate(node))
8788
}
8889
else if (node.type === 'Operator' && node.value === ',') {
8990
// Start a new shadow, but only after we've made sure that the current shadow is valid
@@ -123,7 +124,7 @@ function complete_shadow_token(token: DestructuredShadow) {
123124
token.spread = DIMENSION_ZERO
124125
}
125126
if (!token.color) {
126-
token.color = '#000'
127+
token.color = color_to_token('#000')
127128
}
128129
return token
129130
}

src/index.test.ts

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,15 @@ describe('analysis_to_tokens', () => {
1818
let expected = {
1919
color: {
2020
'green-5e0cf03': {
21-
$value: 'green'
21+
$type: 'color',
22+
$value: {
23+
colorSpace: 'srgb',
24+
components: [0, 0.5019607843137255, 0],
25+
alpha: 1,
26+
},
27+
$extensions: {
28+
[EXTENSION_AUTHORED_AS]: 'green'
29+
}
2230
},
2331
},
2432
font_size: {
@@ -69,16 +77,32 @@ describe('css_to_tokens', () => {
6977
let actual = css_to_tokens(`
7078
.my-design-system {
7179
color: green;
72-
color: rgb(100 100 100 / 0.2);
80+
color: rgb(100 100 100 / 20%);
7381
}
7482
`)
7583
expect(actual.color).toEqual({
7684
'green-5e0cf03': {
77-
$value: 'green'
78-
},
79-
'grey-812aeee': {
80-
$value: 'rgb(100 100 100 / 0.2)'
85+
$type: 'color',
86+
$value: {
87+
colorSpace: 'srgb',
88+
components: [0, 0.5019607843137255, 0],
89+
alpha: 1,
90+
},
91+
$extensions: {
92+
[EXTENSION_AUTHORED_AS]: 'green'
93+
}
8194
},
95+
'grey-8139d9b': {
96+
$type: 'color',
97+
$value: {
98+
colorSpace: 'srgb',
99+
components: [0.39215686274509803, 0.39215686274509803, 0.39215686274509803],
100+
alpha: 0.2,
101+
},
102+
$extensions: {
103+
[EXTENSION_AUTHORED_AS]: 'rgb(100 100 100 / 20%)'
104+
},
105+
}
82106
})
83107
})
84108
})
@@ -242,7 +266,17 @@ describe('css_to_tokens', () => {
242266
unit: 'px'
243267
},
244268
inset: false,
245-
color: 'rgba(0, 0, 0, 0.5)',
269+
color: {
270+
$type: 'color',
271+
$value: {
272+
colorSpace: 'srgb',
273+
components: [0, 0, 0],
274+
alpha: 0.5,
275+
},
276+
$extensions: {
277+
[EXTENSION_AUTHORED_AS]: 'rgba(0, 0, 0, 0.5)'
278+
}
279+
},
246280
},
247281
$extensions: {
248282
[EXTENSION_AUTHORED_AS]: '0 0 10px 0 rgba(0, 0, 0, 0.5)'
@@ -283,7 +317,17 @@ describe('css_to_tokens', () => {
283317
unit: 'px'
284318
},
285319
inset: false,
286-
color: 'rgba(0, 0, 0, 0.5)'
320+
color: {
321+
$type: 'color',
322+
$value: {
323+
colorSpace: 'srgb',
324+
components: [0, 0, 0],
325+
alpha: 0.5,
326+
},
327+
$extensions: {
328+
[EXTENSION_AUTHORED_AS]: 'rgba(0, 0, 0, 0.5)'
329+
}
330+
},
287331
},
288332
{
289333
offsetX: {
@@ -307,7 +351,17 @@ describe('css_to_tokens', () => {
307351
unit: 'px'
308352
},
309353
inset: false,
310-
color: 'rgba(0, 0, 0, 0.5)'
354+
color: {
355+
$type: 'color',
356+
$value: {
357+
colorSpace: 'srgb',
358+
components: [0, 0, 0],
359+
alpha: 0.5,
360+
},
361+
$extensions: {
362+
[EXTENSION_AUTHORED_AS]: 'rgba(0, 0, 0, 0.5)'
363+
}
364+
},
311365
}
312366
],
313367
$extensions: {

src/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { destructure_font_family } from './destructure-font-family.js'
77
import { hash } from './hash.js'
88
import { destructure_line_height } from './destructure-line-height.js'
99
import { parse_length } from './parse-length.js'
10-
import type { CssAnalysis } from './types.js'
10+
import type { ColorToken, CssAnalysis } from './types.js'
1111
import {
1212
EXTENSION_AUTHORED_AS,
1313
type CubicBezierToken,
@@ -18,6 +18,7 @@ import {
1818
type BaseToken,
1919
type UnparsedToken,
2020
} from './types.js'
21+
import { color_to_token } from './colors.js'
2122

2223
const TYPE_CUBIC_BEZIER = 'cubicBezier' as const
2324

@@ -54,15 +55,14 @@ function get_unique(collection: Collection) {
5455
export function analysis_to_tokens(analysis: CssAnalysis) {
5556
return {
5657
color: (() => {
57-
let colors = Object.create(null) as Record<string, UnparsedToken>
58+
let colors = Object.create(null) as Record<string, ColorToken>
5859
let unique = get_unique(analysis.values.colors)
5960
let grouped_colors = group_colors(unique)
6061

6162
for (let [group, group_colors] of grouped_colors) {
6263
for (let color of group_colors) {
63-
colors[`${color_dict.get(group)}-${hash(color.authored)}`] = {
64-
$value: color.authored
65-
}
64+
let color_token = color_to_token(color.authored)
65+
colors[`${color_dict.get(group)}-${hash(color.authored)}`] = color_token
6666
}
6767
}
6868
return colors

0 commit comments

Comments
 (0)