Skip to content

Commit 99141c0

Browse files
HerrTopiclaude
andcommitted
feat(emotion,ui-scripts): resolve tokens studio color modifiers and emit hex
Forward Tokens Studio color modifiers (`$extensions.studio.tokens.modify`) through the theme build scripts and resolve them at theme-apply time so semantic and component tokens can chain darken/lighten/alpha. - generateSemantics: emit the `{ value, modify }` payload for semantic tokens and recurse into it in resolveReferences (drop the `.value`/`.type` guard that serialized the payload to "[object Object]"). - generateComponents: only forward `$extensions` when `studio.tokens.modify` is present, so tokens carrying unrelated metadata no longer emit `modify: undefined`. - applyColorModifiers: serialize the resolved color to hex (`#RRGGBBAA` when alpha < 1, `#RRGGBB` otherwise) so modifier-derived values match every other token. - useComputedTheme/useStyleNew/withStyleNew: resolve modifiers on semantics before building component and shared tokens, so component-level modifiers chain on top of already-resolved semantic values; key component overrides by the component's own id rather than the token-source id. - Bump @instructure/instructure-design-tokens to v1.4.0. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 4534fa0 commit 99141c0

10 files changed

Lines changed: 83 additions & 54 deletions

File tree

packages/emotion/src/styleUtils/applyColorModifiers.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@
2121
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2222
* SOFTWARE.
2323
*/
24-
import { colorToHsla } from '@instructure/ui-color-utils'
24+
import {
25+
colorToHsla,
26+
color2hex,
27+
colorToHex8
28+
} from '@instructure/ui-color-utils'
2529

2630
type ModifyColor = {
2731
value: string
@@ -43,13 +47,11 @@ const modifyLightness = (
4347
? Math.max(0, l - l * amount)
4448
: Math.min(1, l + (1 - l) * amount)
4549

46-
const formatHsla = (h: number, s: number, l: number, a: number): string => {
47-
const hh = Math.round(h)
48-
const ss = +(s * 100).toFixed(2)
49-
const ll = +(l * 100).toFixed(2)
50-
return a < 1
51-
? `hsla(${hh}, ${ss}%, ${ll}%, ${a})`
52-
: `hsl(${hh}, ${ss}%, ${ll}%)`
50+
const formatColor = (h: number, s: number, l: number, a: number): string => {
51+
const hsla = `hsla(${Math.round(h)}, ${+(s * 100).toFixed(2)}%, ${+(
52+
l * 100
53+
).toFixed(2)}%, ${a})`
54+
return a < 1 ? colorToHex8(hsla) : color2hex(hsla)
5355
}
5456

5557
const isModifyColor = (val: unknown): val is ModifyColor =>
@@ -63,12 +65,12 @@ const resolveModifyColor = ({ value, modify }: ModifyColor): string => {
6365
if (modify.type === 'darken' || modify.type === 'lighten') {
6466
const { h, s, l, a } = colorToHsla(value)
6567
const newL = modifyLightness(l, modify.value, modify.type)
66-
return formatHsla(h, s, newL, a)
68+
return formatColor(h, s, newL, a)
6769
}
6870
if (modify.type === 'alpha') {
6971
const { h, s, l } = colorToHsla(value)
7072
const newA = Math.min(1, Math.max(0, Number(modify.value)))
71-
return formatHsla(h, s, l, newA)
73+
return formatColor(h, s, l, newA)
7274
}
7375
return value
7476
}

packages/emotion/src/useComputedTheme.ts

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
*/
2424

2525
import { useTheme } from '@emotion/react'
26+
import { applyColorModifiers } from './styleUtils/applyColorModifiers.js'
2627

2728
/**
2829
* ---
@@ -40,23 +41,25 @@ import { useTheme } from '@emotion/react'
4041
* `components` and `sharedTokens` of the current theme.
4142
*/
4243
export const useComputedTheme = () => {
43-
const rawTheme = (useTheme() as any).newTheme
44+
const rawTheme = (useTheme() as any).newTheme
4445

45-
const primitives = rawTheme?.primitives
46-
const semantics = rawTheme?.semantics?.(primitives)
47-
const components = Object.keys(rawTheme?.components).reduce(
48-
(acc, component) => ({
49-
...acc,
50-
[component]: rawTheme.components[component]?.(semantics)
51-
}),
52-
{}
46+
const primitives = rawTheme?.primitives
47+
const semantics = applyColorModifiers(rawTheme?.semantics?.(primitives))
48+
const components = applyColorModifiers(
49+
Object.keys(rawTheme?.components).reduce(
50+
(acc, component) => ({
51+
...acc,
52+
[component]: rawTheme.components[component]?.(semantics)
53+
}),
54+
{}
5355
)
54-
const sharedTokens = rawTheme?.sharedTokens?.(semantics)
56+
)
57+
const sharedTokens = applyColorModifiers(rawTheme?.sharedTokens?.(semantics))
5558

56-
return {
57-
primitives,
58-
semantics,
59-
components,
60-
sharedTokens
61-
}
62-
}
59+
return {
60+
primitives,
61+
semantics,
62+
components,
63+
sharedTokens
64+
}
65+
}

packages/emotion/src/useStyleNew.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,18 +108,21 @@ const useStyleNew = <
108108
const semanticsOverrides = themeOverrideFromProvider?.semantics
109109
const sharedTokensOverrides = themeOverrideFromProvider?.sharedTokens
110110
const componentOverridesFromSettingsProvider =
111-
themeOverrideFromProvider?.components?.[componentWithTokensId]
111+
themeOverrideFromProvider?.components?.[
112+
componentId as keyof NewComponentTypes
113+
]
112114

113115
const primitives = mergeDeep(theme.newTheme.primitives, primitiveOverrides!)
114116

115-
const semantics = mergeDeep(
116-
theme.newTheme.semantics?.(primitives),
117-
semanticsOverrides!
117+
const semantics = applyColorModifiers(
118+
mergeDeep(theme.newTheme.semantics?.(primitives), semanticsOverrides!)
118119
)
119120

120-
const sharedTokens = mergeDeep(
121-
theme.newTheme.sharedTokens?.(semantics),
122-
sharedTokensOverrides as Record<string, unknown>
121+
const sharedTokens = applyColorModifiers(
122+
mergeDeep(
123+
theme.newTheme.sharedTokens?.(semantics),
124+
sharedTokensOverrides as Record<string, unknown>
125+
)
123126
)
124127

125128
const baseComponentTheme = applyColorModifiers(

packages/emotion/src/withStyleNew.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,10 @@ const withStyleNew = decorator(
132132
) => {
133133
const displayName = ComposedComponent.displayName || ComposedComponent.name
134134

135-
const componentId: keyof NewComponentTypes =
136-
useTokensFrom ?? ComposedComponent.componentId?.replace('.', '')
135+
const rawComponentId: keyof NewComponentTypes =
136+
ComposedComponent.componentId?.replace('.', '')
137+
138+
const componentId: keyof NewComponentTypes = useTokensFrom ?? rawComponentId
137139

138140
const WithStyle: ForwardRefExoticComponent<
139141
PropsWithoutRef<Props> & RefAttributes<any>
@@ -188,7 +190,7 @@ const withStyleNew = decorator(
188190
const semanticsOverrides = themeOverride?.semantics
189191
const sharedTokensOverrides = themeOverride?.sharedTokens
190192
const componentOverridesFromSettingsProvider =
191-
themeOverride?.components?.[componentId]
193+
themeOverride?.components?.[rawComponentId]
192194
const componentOverridesFromThemeOverrideProp = (
193195
componentProps as ThemeOverrideProp
194196
).themeOverride
@@ -198,14 +200,15 @@ const withStyleNew = decorator(
198200
primitiveOverrides!
199201
)
200202

201-
const semantics = mergeDeep(
202-
theme.newTheme.semantics?.(primitives),
203-
semanticsOverrides!
203+
const semantics = applyColorModifiers(
204+
mergeDeep(theme.newTheme.semantics?.(primitives), semanticsOverrides!)
204205
)
205206

206-
const sharedTokens = mergeDeep(
207-
theme.newTheme.sharedTokens?.(semantics),
208-
sharedTokensOverrides as Record<string, unknown>
207+
const sharedTokens = applyColorModifiers(
208+
mergeDeep(
209+
theme.newTheme.sharedTokens?.(semantics),
210+
sharedTokensOverrides as Record<string, unknown>
211+
)
209212
) as SharedTokens
210213
// Note: Some components do not have a theme, e.g., FormFieldMessages
211214
const baseComponentTheme = applyColorModifiers(

packages/ui-scripts/lib/build/buildThemes/generateComponents.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ const formatComponent = (collection: any, key?: string): any => {
3636
// metadata. Tokens Studio writes color modifiers there under `studio.tokens.modify`
3737
// (e.g. `{ type: 'darken', value: 0.1 }`); we forward that payload so the runtime
3838
// (`applyColorModifiers`) can resolve the final color at theme apply time.
39-
if (value['$extensions']) {
39+
if (
40+
value['$extensions'] &&
41+
value['$extensions']['studio.tokens'] &&
42+
value['$extensions']['studio.tokens'].modify
43+
) {
4044
return {
4145
value: value.value,
4246
modify: value['$extensions']['studio.tokens'].modify

packages/ui-scripts/lib/build/buildThemes/generateSemantics.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,20 @@ const formatSemantic = (collection: any, key?: any): any => {
9191
return { ...acc, [key]: formatSemantic(value, key) }
9292
}, {})
9393
}
94+
// `$extensions` is the Design Tokens Format spec's escape hatch for tool-specific
95+
// metadata. Tokens Studio writes color modifiers there under `studio.tokens.modify`
96+
// (e.g. `{ type: 'darken', value: 0.1 }`); we forward that payload so the runtime
97+
// (`applyColorModifiers`) can resolve the final color at theme apply time.
98+
if (
99+
value['$extensions'] &&
100+
value['$extensions']['studio.tokens'] &&
101+
value['$extensions']['studio.tokens'].modify
102+
) {
103+
return {
104+
value: value.value,
105+
modify: value['$extensions']['studio.tokens'].modify
106+
}
107+
}
94108
return value.value
95109
}
96110

@@ -108,7 +122,7 @@ const formatReference = (reference: string): string => {
108122

109123
export const resolveReferences = (semantics: any, key?: any): string => {
110124
const value = key ? semantics[key] : semantics
111-
if (typeof value === 'object' && !value.value && !value.type) {
125+
if (typeof value === 'object') {
112126
return Object.keys(value).reduce((acc, key, index) => {
113127
if (typeof value[key] === 'object') {
114128
return (

packages/ui-scripts/lib/build/buildThemes/setupThemes.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,8 @@ const setupThemes = async (targetPath: string, input: any): Promise<void> => {
197197
import type { Semantics } from "./semantics"
198198
199199
const ${fullComponentName} = (semantic: Semantics): ${capitalize(
200-
fullComponentName
201-
)} => ({${componentThemeVars}})
200+
fullComponentName
201+
)} => ({${componentThemeVars}})
202202
export default ${fullComponentName}
203203
`
204204

packages/ui-scripts/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"dependencies": {
2424
"@babel/cli": "^7.27.2",
2525
"@instructure/command-utils": "workspace:*",
26-
"@instructure/instructure-design-tokens": "github:instructure/instructure-design-tokens#v1.3.0",
26+
"@instructure/instructure-design-tokens": "github:instructure/instructure-design-tokens#v1.4.0",
2727
"dprint": "^0.55.1",
2828
"http-server": "^14.1.1",
2929
"lerna": "9.0.7",

pnpm-lock.yaml

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

regression-test/package-lock.json

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

0 commit comments

Comments
 (0)