Skip to content

Commit 32fbee0

Browse files
committed
feat: add colorSchemePreview setting for light-dark() values
Adds a new setting `tailwindCSS.colorSchemePreview` that controls whether the light or dark color is shown in previews for CSS `light-dark()` function values. - Accepts 'light' (default) or 'dark' - Useful for developers working primarily in one color scheme - Added tests for the resolveLightDark function https://claude.ai/code/session_01Fi6Utr1yb5jk3Xo9yvY567
1 parent 4babab3 commit 32fbee0

5 files changed

Lines changed: 93 additions & 17 deletions

File tree

packages/tailwindcss-language-service/src/documentColorProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export async function getDocumentColors(
2525
classLists.forEach((classList) => {
2626
let classNames = getClassNamesInClassList(classList, state.blocklist)
2727
classNames.forEach((className) => {
28-
let color = getColor(state, className.className)
28+
let color = getColor(state, className.className, settings.tailwindCSS.colorSchemePreview)
2929
if (color === null || typeof color === 'string' || (color.alpha ?? 1) === 0) {
3030
return
3131
}

packages/tailwindcss-language-service/src/util/color.test.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { test, expect } from 'vitest'
1+
import { test, expect, describe } from 'vitest'
22
import namedColors from 'color-name'
3-
import { findColors } from './color'
3+
import { findColors, resolveLightDark } from './color'
44

55
let table: string[] = []
66

@@ -80,3 +80,53 @@ test('invalid hex', () => {
8080
expect(findColors(`#7f7f7fz`)).toEqual([])
8181
expect(findColorsRegex(`#7f7f7fz`)).toEqual([])
8282
})
83+
84+
describe('resolveLightDark', () => {
85+
test('extracts light color by default', () => {
86+
const input = 'light-dark(oklch(0.5 0.1 50), oklch(0.8 0.2 60))'
87+
expect(resolveLightDark(input)).toBe('oklch(0.5 0.1 50)')
88+
})
89+
90+
test('extracts light color when colorScheme is light', () => {
91+
const input = 'light-dark(oklch(0.5 0.1 50), oklch(0.8 0.2 60))'
92+
expect(resolveLightDark(input, 'light')).toBe('oklch(0.5 0.1 50)')
93+
})
94+
95+
test('extracts dark color when colorScheme is dark', () => {
96+
const input = 'light-dark(oklch(0.5 0.1 50), oklch(0.8 0.2 60))'
97+
expect(resolveLightDark(input, 'dark')).toBe('oklch(0.8 0.2 60)')
98+
})
99+
100+
test('handles hex colors', () => {
101+
const input = 'light-dark(#ffffff, #000000)'
102+
expect(resolveLightDark(input, 'light')).toBe('#ffffff')
103+
expect(resolveLightDark(input, 'dark')).toBe('#000000')
104+
})
105+
106+
test('handles rgb colors', () => {
107+
const input = 'light-dark(rgb(255 255 255), rgb(0 0 0))'
108+
expect(resolveLightDark(input, 'light')).toBe('rgb(255 255 255)')
109+
expect(resolveLightDark(input, 'dark')).toBe('rgb(0 0 0)')
110+
})
111+
112+
test('handles multiple light-dark functions in string', () => {
113+
const input =
114+
'color: light-dark(#fff, #000); background: light-dark(#eee, #111);'
115+
expect(resolveLightDark(input, 'light')).toBe(
116+
'color: #fff; background: #eee;',
117+
)
118+
expect(resolveLightDark(input, 'dark')).toBe('color: #000; background: #111;')
119+
})
120+
121+
test('trims whitespace from colors', () => {
122+
const input = 'light-dark( #ffffff , #000000 )'
123+
expect(resolveLightDark(input, 'light')).toBe('#ffffff')
124+
expect(resolveLightDark(input, 'dark')).toBe('#000000')
125+
})
126+
127+
test('returns input unchanged when no light-dark function', () => {
128+
const input = '#ffffff'
129+
expect(resolveLightDark(input, 'light')).toBe('#ffffff')
130+
expect(resolveLightDark(input, 'dark')).toBe('#ffffff')
131+
})
132+
})

packages/tailwindcss-language-service/src/util/color.ts

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,24 @@ function getKeywordColor(value: unknown): KeywordColor | null {
4949
return null
5050
}
5151

52-
function getColorsInString(state: State, str: string): ParsedColor[] {
52+
function getColorsInString(
53+
state: State,
54+
str: string,
55+
colorScheme: 'light' | 'dark' = 'light',
56+
): ParsedColor[] {
5357
if (/(?:box|drop)-shadow/.test(str) && !/--tw-drop-shadow/.test(str)) return []
5458

5559
str = replaceCssVarsWithFallbacks(state, str)
5660
str = removeColorMixWherePossible(str)
57-
str = resolveLightDark(str)
61+
str = resolveLightDark(str, colorScheme)
5862

5963
return parseColors(str)
6064
}
6165

6266
function getColorFromDecls(
6367
state: State,
6468
decls: Record<string, string | string[]>,
69+
colorScheme: 'light' | 'dark' = 'light',
6570
): ParsedColor | null {
6671
let props = Object.keys(decls).filter((prop) => {
6772
// ignore content: "";
@@ -104,7 +109,7 @@ function getColorFromDecls(
104109
const propsToCheck = areAllCustom ? props : nonCustomProps
105110

106111
const colors = propsToCheck.flatMap((prop) =>
107-
ensureArray(decls[prop]).flatMap((str) => getColorsInString(state, str)),
112+
ensureArray(decls[prop]).flatMap((str) => getColorsInString(state, str, colorScheme)),
108113
)
109114

110115
// check that all of the values are the same color, ignoring alpha
@@ -137,7 +142,11 @@ function getColorFromDecls(
137142
return null
138143
}
139144

140-
function getColorFromRoot(state: State, css: AstNode[]): ParsedColor | null {
145+
function getColorFromRoot(
146+
state: State,
147+
css: AstNode[],
148+
colorScheme: 'light' | 'dark' = 'light',
149+
): ParsedColor | null {
141150
let decls: Record<string, string[]> = {}
142151

143152
walk(css, (node) => {
@@ -171,7 +180,7 @@ function getColorFromRoot(state: State, css: AstNode[]): ParsedColor | null {
171180
return WalkAction.Continue
172181
})
173182

174-
return getColorFromDecls(state, decls)
183+
return getColorFromDecls(state, decls, colorScheme)
175184
}
176185

177186
let isNegative = /^-/
@@ -190,13 +199,17 @@ function isLikelyColorless(className: string) {
190199
return false
191200
}
192201

193-
export function getColor(state: State, className: string): ParsedColor | null {
202+
export function getColor(
203+
state: State,
204+
className: string,
205+
colorScheme: 'light' | 'dark' = 'light',
206+
): ParsedColor | null {
194207
if (state.v4) {
195208
// FIXME: This is a performance optimization and not strictly correct
196209
if (isLikelyColorless(className)) return null
197210

198211
let css = state.designSystem.compile([className])[0]
199-
let color = getColorFromRoot(state, css)
212+
let color = getColorFromRoot(state, css, colorScheme)
200213

201214
let prefix = state.designSystem.theme.prefix ?? ''
202215

@@ -205,7 +218,7 @@ export function getColor(state: State, className: string): ParsedColor | null {
205218
if (prefix && !color && !className.startsWith(prefix + ':')) {
206219
className = `${prefix}:${className}`
207220
css = state.designSystem.compile([className])[0]
208-
color = getColorFromRoot(state, css)
221+
color = getColorFromRoot(state, css, colorScheme)
209222
}
210223

211224
return color
@@ -215,7 +228,7 @@ export function getColor(state: State, className: string): ParsedColor | null {
215228
if (state.classNames) {
216229
const item = dlv(state.classNames.classNames, [className, '__info'])
217230
if (item && item.__rule) {
218-
return getColorFromDecls(state, removeMeta(item))
231+
return getColorFromDecls(state, removeMeta(item), colorScheme)
219232
}
220233
}
221234

@@ -244,7 +257,7 @@ export function getColor(state: State, className: string): ParsedColor | null {
244257
decls[decl.prop] = decl.value
245258
}
246259
})
247-
return getColorFromDecls(state, decls)
260+
return getColorFromDecls(state, decls, colorScheme)
248261
}
249262

250263
let parts = getClassNameParts(state, className)
@@ -253,7 +266,7 @@ export function getColor(state: State, className: string): ParsedColor | null {
253266
const item = dlv(state.classNames.classNames, [...parts, '__info'])
254267
if (!item.__rule) return null
255268

256-
return getColorFromDecls(state, removeMeta(item))
269+
return getColorFromDecls(state, removeMeta(item), colorScheme)
257270
}
258271

259272
export function getColorFromValue(value: unknown): ParsedColor | null {
@@ -315,10 +328,15 @@ function removeColorMixWherePossible(str: string) {
315328
})
316329
}
317330

318-
const LIGHT_DARK_REGEX = /light-dark\(\s*(.*?)\s*,\s*.*?\s*\)/g
331+
const LIGHT_DARK_REGEX = /light-dark\(\s*([^,]+?)\s*,\s*([^,]+)\s*\)/g
319332

320-
function resolveLightDark(str: string) {
321-
return str.replace(LIGHT_DARK_REGEX, (_, lightColor) => lightColor)
333+
export function resolveLightDark(
334+
str: string,
335+
colorScheme: 'light' | 'dark' = 'light',
336+
): string {
337+
return str.replace(LIGHT_DARK_REGEX, (_, lightColor, darkColor) =>
338+
colorScheme === 'light' ? lightColor.trim() : darkColor.trim(),
339+
)
322340
}
323341

324342
const COLOR_FNS = new Set([

packages/tailwindcss-language-service/src/util/state.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export type TailwindCssSettings = {
5656
showPixelEquivalents: boolean
5757
rootFontSize: number
5858
colorDecorators: boolean
59+
colorSchemePreview: 'light' | 'dark'
5960
lint: {
6061
cssConflict: DiagnosticSeveritySetting
6162
invalidApply: DiagnosticSeveritySetting
@@ -195,6 +196,7 @@ export function getDefaultTailwindSettings(): Settings {
195196
suggestions: true,
196197
validate: true,
197198
colorDecorators: true,
199+
colorSchemePreview: 'light',
198200
rootFontSize: 16,
199201
lint: {
200202
cssConflict: 'warning',

packages/vscode-tailwindcss/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,12 @@
222222
"markdownDescription": "Controls whether the editor should render inline color decorators for Tailwind CSS classes and helper functions.",
223223
"scope": "language-overridable"
224224
},
225+
"tailwindCSS.colorSchemePreview": {
226+
"type": "string",
227+
"enum": ["light", "dark"],
228+
"default": "light",
229+
"markdownDescription": "Which color value to preview for `light-dark()` CSS functions."
230+
},
225231
"tailwindCSS.validate": {
226232
"type": "boolean",
227233
"default": true,

0 commit comments

Comments
 (0)