diff --git a/.changeset/fix-semantic-token-lookup-10822.md b/.changeset/fix-semantic-token-lookup-10822.md new file mode 100644 index 00000000000..b3d23965f28 --- /dev/null +++ b/.changeset/fix-semantic-token-lookup-10822.md @@ -0,0 +1,9 @@ +--- +"@chakra-ui/react": patch +--- + +Fix `system.token()` returning dark-mode resolved values for semantic tokens +with light/dark conditions instead of the semantic CSS variable reference. + +Also fix token dictionary bookkeeping for semantic tokens without a base value +so lookup maps stay in sync after empty tokens are removed. diff --git a/packages/react/__tests__/system.test.ts b/packages/react/__tests__/system.test.ts index c1bf132be34..ff27f2c60f9 100644 --- a/packages/react/__tests__/system.test.ts +++ b/packages/react/__tests__/system.test.ts @@ -26,7 +26,6 @@ describe("system", () => { "@layer tokens": { "&:where(html)": { "--chakra-colors-primary": "#000", - "--chakra-colors-test": "", }, ".dark &": { "--chakra-colors-test": "pink", @@ -217,4 +216,76 @@ describe("system", () => { } `) }) + + test("system.token returns semantic css var for conditional color tokens (#10822)", () => { + const tokens = { + colors: { + teal: { + 200: { value: "#light" }, + 800: { value: "#dark" }, + }, + }, + } + + const semanticTokens = { + colors: { + teal: { + muted: { + value: { + _light: "{colors.teal.200}", + _dark: "{colors.teal.800}", + }, + }, + }, + }, + } + + const sys = createSystem({ theme: { tokens, semanticTokens } }) + + expect(sys.token("colors.teal.muted")).toBe( + "var(--chakra-colors-teal-muted)", + ) + expect(sys.token.var("colors.teal.muted")).toBe( + "var(--chakra-colors-teal-muted)", + ) + expect(sys.query.semanticTokens.list("colors")).toContain("teal.muted") + expect(sys.token("colors.teal.200")).toBe("#light") + }) + + test("system.token resolves semantic tokens with base condition (#10822)", () => { + const tokens = { + colors: { + teal: { + 200: { value: "#light" }, + 800: { value: "#dark" }, + }, + }, + } + + const semanticTokens = { + colors: { + accent: { + value: { + base: "{colors.teal.200}", + _dark: "{colors.teal.800}", + }, + }, + plain: { + value: { base: "#fff" }, + }, + nested: { + DEFAULT: { + value: "{colors.accent}", + }, + }, + }, + } + + const sys = createSystem({ theme: { tokens, semanticTokens } }) + + expect(sys.token("colors.accent")).toBe("var(--chakra-colors-accent)") + expect(sys.token("colors.plain")).toBe("var(--chakra-colors-plain)") + expect(sys.token("colors.nested")).toBe("var(--chakra-colors-nested)") + expect(sys.token("colors.teal.200")).toBe("#light") + }) }) diff --git a/packages/react/__tests__/token-dictionary.test.ts b/packages/react/__tests__/token-dictionary.test.ts index 9462e76edc1..c101313f2ce 100644 --- a/packages/react/__tests__/token-dictionary.test.ts +++ b/packages/react/__tests__/token-dictionary.test.ts @@ -138,6 +138,63 @@ describe("token dictionary", () => { expect(token?.extensions.conditions).toEqual({ base: "red", _dark: "blue" }) }) + test("keeps conditional semantic token views in sync", () => { + const dict = createTokenDictionary({ + prefix: "chakra", + tokens: { + colors: { + teal: { + 200: { value: "#light" }, + 800: { value: "#dark" }, + }, + }, + }, + semanticTokens: { + colors: { + teal: { + muted: { + value: { + _light: "{colors.teal.200}", + _dark: "{colors.teal.800}", + }, + }, + }, + }, + }, + }) + + const conditions = { + _light: "{colors.teal.200}", + _dark: "{colors.teal.800}", + } + + const token = dict.getByName("colors.teal.muted") + const categoryToken = dict.categoryMap.get("colors")?.get("teal.muted") + const darkToken = dict.allTokens.find( + (token) => + token.name === "colors.teal.muted" && + token.extensions.condition === "_dark", + ) + + expect(token).toBeDefined() + expect(dict.allTokens).toContain(token) + expect(token?.extensions.conditions).toEqual(conditions) + + expect(categoryToken).toBe(token) + expect(dict.allTokens).toContain(categoryToken) + + expect(darkToken?.extensions.conditions).toEqual(conditions) + expect(dict.cssVarMap.get("base")?.has("--chakra-colors-teal-muted")).toBe( + false, + ) + expect( + dict.cssVarMap.get("_light")?.get("--chakra-colors-teal-muted"), + ).toBe("var(--chakra-colors-teal-200)") + expect(dict.cssVarMap.get("_dark")?.get("--chakra-colors-teal-muted")).toBe( + "var(--chakra-colors-teal-800)", + ) + }) + test("semantic token references preserve conditional token references", () => { const dict = createTokenDictionary({ prefix: "chakra", diff --git a/packages/react/src/styled-system/system.ts b/packages/react/src/styled-system/system.ts index 2b85426a534..e5d4b1591d1 100644 --- a/packages/react/src/styled-system/system.ts +++ b/packages/react/src/styled-system/system.ts @@ -256,12 +256,17 @@ export function createSystem(...configs: SystemConfig[]): SystemContext { function getTokenMap(tokens: TokenDictionary) { const map = new Map() + const names = new Set(tokens.allTokens.map((token) => token.name)) + + for (const name of names) { + const token = tokens.getByName(name) + if (!token?.extensions.cssVar) continue - tokens.allTokens.forEach((token) => { const { cssVar, virtual, conditions } = token.extensions - const value = !!conditions || virtual ? cssVar!.ref : token.value - map.set(token.name, { value, variable: cssVar!.ref }) - }) + const isSemantic = !!conditions || virtual + const value = isSemantic ? cssVar.ref : token.value + map.set(name, { value, variable: cssVar.ref }) + } return map } diff --git a/packages/react/src/styled-system/token-dictionary.ts b/packages/react/src/styled-system/token-dictionary.ts index 96b12b6f435..c96172a9b47 100644 --- a/packages/react/src/styled-system/token-dictionary.ts +++ b/packages/react/src/styled-system/token-dictionary.ts @@ -8,7 +8,6 @@ import { mapEntries, mapObject, memo, - omit, walkObject, } from "../utils" import { cssVar } from "./css-var" @@ -205,21 +204,30 @@ export function createTokenDictionary(options: Options): TokenDictionary { } function buildCategoryMap(token: Token) { - const { category, prop, condition } = token.extensions + const { category, prop, condition, conditions } = token.extensions if (!category) return - if (condition != null && condition !== "base") return - if (!categoryMap.has(category)) { categoryMap.set(category, new Map()) } - categoryMap.get(category)!.set(prop, token) + const map = categoryMap.get(category)! + const existing = map.get(prop) + + if (condition == null || condition === "base") { + map.set(prop, token) + return + } + + if (conditions && !existing) { + map.set(prop, token) + } } function buildCssVars(token: Token) { const { condition, negative, virtual, cssVar } = token.extensions - if (negative || virtual || !condition || !cssVar) return + if (negative || virtual || !condition || !cssVar || token.value === "") + return if (!cssVarMap.has(condition)) { cssVarMap.set(condition, new Map()) @@ -525,7 +533,7 @@ function getConditionalTokens(token: Token) { ...token, value, extensions: { - ...omit(token.extensions, ["conditions"]), + ...token.extensions, condition: nextPath.join(":"), }, } diff --git a/packages/react/src/styled-system/token-middleware.ts b/packages/react/src/styled-system/token-middleware.ts index b0c944e7ccb..b2a7f3a9c8e 100644 --- a/packages/react/src/styled-system/token-middleware.ts +++ b/packages/react/src/styled-system/token-middleware.ts @@ -134,9 +134,37 @@ export const addVirtualPalette: TokenMiddleware = { export const removeEmptyTokens: TokenMiddleware = { enforce: "post", transform(dictionary) { - dictionary.allTokens = dictionary.allTokens.filter( - (token) => token.value !== "", - ) + const removed: Token[] = [] + const next: Token[] = [] + + dictionary.allTokens.forEach((token) => { + if (token.value === "") { + removed.push(token) + } else { + next.push(token) + } + }) + + dictionary.allTokens.splice(0, dictionary.allTokens.length, ...next) + + removed.forEach((token) => { + if (dictionary.tokenMap.get(token.name) !== token) return + + const replacement = dictionary.allTokens.find( + (t) => t.name === token.name, + ) + if (!replacement) { + dictionary.tokenMap.delete(token.name) + return + } + + // Safety net for non-standard registration paths that omit conditions. + if (token.extensions.conditions && !replacement.extensions.conditions) { + replacement.extensions.conditions = token.extensions.conditions + } + + dictionary.tokenMap.set(token.name, replacement) + }) }, } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a0ee10e5177..9b78c849633 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5966,9 +5966,6 @@ packages: caniuse-lite@1.0.30001754: resolution: {integrity: sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==} - caniuse-lite@1.0.30001766: - resolution: {integrity: sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==} - caniuse-lite@1.0.30001793: resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} @@ -10782,10 +10779,6 @@ packages: stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} - strip-ansi@6.0.0: - resolution: {integrity: sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==} - engines: {node: '>=8'} - strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -17177,7 +17170,7 @@ snapshots: browserslist@4.28.0: dependencies: baseline-browser-mapping: 2.8.28 - caniuse-lite: 1.0.30001766 + caniuse-lite: 1.0.30001793 electron-to-chromium: 1.5.252 node-releases: 2.0.27 update-browserslist-db: 1.1.4(browserslist@4.28.0) @@ -17282,8 +17275,6 @@ snapshots: caniuse-lite@1.0.30001754: {} - caniuse-lite@1.0.30001766: {} - caniuse-lite@1.0.30001793: {} ccount@2.0.1: {} @@ -21268,7 +21259,7 @@ snapshots: dependencies: '@next/env': 15.5.18 '@swc/helpers': 0.5.15 - caniuse-lite: 1.0.30001788 + caniuse-lite: 1.0.30001793 postcss: 8.4.31 react: 19.2.6 react-dom: 19.2.6(react@19.2.6) @@ -21506,7 +21497,7 @@ snapshots: is-interactive: 1.0.0 is-unicode-supported: 0.1.0 log-symbols: 4.1.0 - strip-ansi: 6.0.0 + strip-ansi: 6.0.1 wcwidth: 1.0.1 ora@9.3.0: @@ -23366,10 +23357,6 @@ snapshots: character-entities-html4: 2.1.0 character-entities-legacy: 3.0.0 - strip-ansi@6.0.0: - dependencies: - ansi-regex: 5.0.1 - strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -24196,7 +24183,7 @@ snapshots: vite@5.4.21(@types/node@24.10.12)(lightningcss@1.32.0)(terser@5.46.0): dependencies: esbuild: 0.21.5 - postcss: 8.5.10 + postcss: 8.5.14 rollup: 4.57.1 optionalDependencies: '@types/node': 24.10.12