Skip to content

Commit 0c52c1d

Browse files
authored
feat: generate tokens-tailwind.css for design tokens (#7225)
1 parent 407d18a commit 0c52c1d

File tree

6 files changed

+964
-43
lines changed

6 files changed

+964
-43
lines changed

packages/dnb-design-system-portal/src/docs/uilib/usage/customisation/styling.mdx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,9 @@ You can import the pre-generated `properties-tailwind.css` files that contain al
187187

188188
/* Import Eufemia properties in Tailwind format */
189189
@import '@dnb/eufemia/style/themes/ui/properties-tailwind.css' layer(utilities);
190+
191+
/* Import Eufemia design tokens in Tailwind format */
192+
@import '@dnb/eufemia/style/themes/ui/tokens-tailwind.css' layer(utilities);
190193
```
191194

192195
### Tailwind-Compatible CSS Variables
@@ -225,6 +228,29 @@ For Sbanken, the following namespaces are available:
225228

226229
This makes Eufemia properties directly usable with Tailwind's utility classes while maintaining brand-specific variants.
227230

231+
### Semantic Design Tokens
232+
233+
In addition to the theme properties above, Eufemia provides semantic design tokens via `tokens-tailwind.css`. These tokens represent higher-level design decisions (e.g., "background for an action element") rather than raw values, and reference foundation color variables.
234+
235+
The `--token-` prefix from `tokens.scss` is stripped, so the variables map directly to Tailwind's `--color-*` namespace:
236+
237+
- **`--color-background-*`** - Background colors (e.g., `--color-background-action`, `--color-background-error-subtle`)
238+
- **`--color-text-*`** - Text colors (e.g., `--color-text-neutral`, `--color-text-action`)
239+
- **`--color-icon-*`** - Icon colors (e.g., `--color-icon-neutral`, `--color-icon-action`)
240+
- **`--color-stroke-*`** - Border/stroke colors (e.g., `--color-stroke-action`, `--color-stroke-error`)
241+
- **`--color-decorative-*`** - Decorative colors (e.g., `--color-decorative-first-base`, `--color-decorative-second-muted`)
242+
- **`--color-component-*`** - Component-specific tokens (e.g., `--color-component-button-background-action`)
243+
244+
This allows usage like:
245+
246+
```html
247+
<div class="bg-background-action text-text-neutral border-stroke-action">
248+
Styled with semantic tokens
249+
</div>
250+
```
251+
252+
The `tokens-tailwind.css` files are available for all themes: `ui`, `sbanken`, and `carnegie`.
253+
228254
## Known styling and CSS issues
229255

230256
- Safari, both on mobile and desktop, has a problem where we combine `border-radius` with the usage of `inset` in a `box-shadow`. The solution for now is to not use `inset`, which results in an outer border. This is not ideal as we don't follow the UX guidelines for these browsers. We have a SASS function handling this for us: `@mixin fakeBorder`.

packages/dnb-eufemia/scripts/prebuild/tasks/__tests__/makePropertiesFile.test.ts

Lines changed: 70 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { convertVariablesToTailwindFormat } from '../tailwindTransform'
2-
31
import makePropertiesFile, {
42
extractReferencedCssVariables,
53
transformFigmaAlias,
@@ -18,6 +16,9 @@ describe('makePropertiesFile', () => {
1816
uiFoundation: null,
1917
sbankenFoundation: null,
2018
carnegieFoundation: null,
19+
uiTokensTailwind: null,
20+
sbankenTokensTailwind: null,
21+
carnegieTokensTailwind: null,
2122
}
2223

2324
beforeAll(async () => {
@@ -59,6 +60,19 @@ describe('makePropertiesFile', () => {
5960
path.resolve('src/style/themes/carnegie/foundation.scss'),
6061
'utf-8'
6162
)
63+
64+
global.uiTokensTailwind = fs.readFileSync(
65+
path.resolve('src/style/themes/ui/tokens-tailwind.css'),
66+
'utf-8'
67+
)
68+
global.sbankenTokensTailwind = fs.readFileSync(
69+
path.resolve('src/style/themes/sbanken/tokens-tailwind.css'),
70+
'utf-8'
71+
)
72+
global.carnegieTokensTailwind = fs.readFileSync(
73+
path.resolve('src/style/themes/carnegie/tokens-tailwind.css'),
74+
'utf-8'
75+
)
6276
})
6377
describe('Tokens snapshots for', () => {
6478
it('ui', () => {
@@ -77,6 +91,60 @@ describe('makePropertiesFile', () => {
7791
})
7892
})
7993

94+
describe('Tokens Tailwind CSS Generation', () => {
95+
describe('CSS File Structure', () => {
96+
it('should contain proper CSS file header', () => {
97+
expect(global.uiTokensTailwind).toContain(
98+
'/* This file is auto generated by makePropertiesFile.ts */'
99+
)
100+
expect(global.uiTokensTailwind).toContain(
101+
'/* stylelint-disable-next-line scss/at-rule-no-unknown */'
102+
)
103+
expect(global.uiTokensTailwind).toContain('@theme {')
104+
})
105+
106+
it('should have proper CSS formatting', () => {
107+
expect(global.uiTokensTailwind).toMatch(/}\s*$/)
108+
})
109+
})
110+
111+
describe('Variable Transformation', () => {
112+
it('should strip --token- prefix from variable names', () => {
113+
expect(global.uiTokensTailwind).toContain(
114+
'--color-background-action:'
115+
)
116+
expect(global.uiTokensTailwind).toContain('--color-text-neutral:')
117+
expect(global.uiTokensTailwind).not.toMatch(
118+
/--token-color-[a-zA-Z-]+:/
119+
)
120+
})
121+
122+
it('should preserve var() references to foundation variables', () => {
123+
expect(global.uiTokensTailwind).toContain('var(--dnb-')
124+
})
125+
})
126+
127+
describe('Theme-Specific Content', () => {
128+
it('should generate tokens-tailwind.css for all themes', () => {
129+
expect(global.uiTokensTailwind).toBeTruthy()
130+
expect(global.sbankenTokensTailwind).toBeTruthy()
131+
expect(global.carnegieTokensTailwind).toBeTruthy()
132+
})
133+
134+
it('should contain semantic color tokens', () => {
135+
for (const tailwind of [
136+
global.uiTokensTailwind,
137+
global.sbankenTokensTailwind,
138+
global.carnegieTokensTailwind,
139+
]) {
140+
expect(tailwind).toContain('--color-background-')
141+
expect(tailwind).toContain('--color-text-')
142+
expect(tailwind).toContain('--color-stroke-')
143+
}
144+
})
145+
})
146+
})
147+
80148
describe('Figma file generation', () => {
81149
describe('extractReferencedCssVariables', () => {
82150
it('extracts css variables from var() usage', () => {
@@ -306,47 +374,6 @@ describe('makePropertiesFile', () => {
306374
})
307375
})
308376

309-
describe('convertVariablesToTailwindFormat Function', () => {
310-
it('should convert --sb-* variables to --*-sb-* format', () => {
311-
const input = {
312-
'--sb-color-black': '#000',
313-
'--sb-font-size-small': '0.875rem',
314-
'--sb-line-height-medium': '2rem',
315-
}
316-
const result = convertVariablesToTailwindFormat(input)
317-
318-
expect(result).toEqual({
319-
'--color-sb-black': '#000',
320-
'--text-sb-small': '0.875rem',
321-
'--leading-sb-medium': '2rem',
322-
})
323-
})
324-
325-
it('should handle var() references correctly', () => {
326-
const input = {
327-
'--font-size-lead': 'var(--sb-font-size-medium)',
328-
}
329-
const result = convertVariablesToTailwindFormat(input)
330-
331-
expect(result).toEqual({
332-
'--text-lead': 'var(--text-sb-medium)',
333-
})
334-
})
335-
336-
it('should preserve non-sb variables unchanged', () => {
337-
const input = {
338-
'--color-white': '#fff',
339-
'--font-size-large': '2rem',
340-
}
341-
const result = convertVariablesToTailwindFormat(input)
342-
343-
expect(result).toEqual({
344-
'--color-white': '#fff',
345-
'--text-large': '2rem',
346-
})
347-
})
348-
})
349-
350377
describe('Tailwind CSS Properties Generation', () => {
351378
let uiTailwindResult, sbankenTailwindResult, eiendomTailwindResult
352379

packages/dnb-eufemia/scripts/prebuild/tasks/makePropertiesFile.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,72 @@ const generateCSSVariablesFromFigmaExport = (
384384
}
385385
}
386386

387+
const makeDesignTokenTailwindCSS = async (
388+
/** Root path to the generated tokens.scss file */
389+
tokensScssPath: string,
390+
/** Root path for the generated Tailwind CSS file */
391+
outputPath: string,
392+
/** prefix to strip from variable names (e.g. 'token') */
393+
prefix: string
394+
) => {
395+
try {
396+
const content = await promises.readFile(tokensScssPath, 'utf-8')
397+
398+
// Extract complete CSS declarations (which may span multiple lines)
399+
const prefixPattern = `--${prefix}-`
400+
const declarations: string[] = []
401+
const lines = content.split('\n')
402+
let currentDeclaration = ''
403+
let collecting = false
404+
405+
for (const line of lines) {
406+
const trimmed = line.trim()
407+
408+
if (trimmed.startsWith(prefixPattern)) {
409+
collecting = true
410+
currentDeclaration = trimmed
411+
} else if (collecting) {
412+
currentDeclaration += '\n' + line
413+
}
414+
415+
if (collecting && currentDeclaration.includes(';')) {
416+
// Strip the prefix from the variable name
417+
const strippedDeclaration = currentDeclaration.replace(
418+
new RegExp(`${prefixPattern}`),
419+
'--'
420+
)
421+
declarations.push(` ${strippedDeclaration.trim()}`)
422+
collecting = false
423+
currentDeclaration = ''
424+
}
425+
}
426+
427+
const cssContent = declarations.join('\n')
428+
429+
const tailwindContent = `/* This file is auto generated by makePropertiesFile.ts */
430+
431+
/* stylelint-disable-next-line scss/at-rule-no-unknown */
432+
@theme {
433+
${cssContent}
434+
}\n`
435+
436+
const prettierResult =
437+
String(
438+
await prettier.format(tailwindContent, {
439+
filepath: 'file.css',
440+
...prettierrc,
441+
})
442+
).trim() + '\n'
443+
444+
await promises.writeFile(outputPath, prettierResult)
445+
446+
log.info(`Generated Tailwind CSS file: ${outputPath}`)
447+
} catch (e) {
448+
log.fail(`Failed to generate Tailwind CSS file: ${outputPath}`)
449+
throw e
450+
}
451+
}
452+
387453
const makeDesignTokenSCSS = async (
388454
/** Root path to Figma JSON export file */
389455
inputPath: string,
@@ -528,4 +594,18 @@ const runDesignTokenFactory = async () => {
528594
})
529595
)
530596
)
597+
598+
await Promise.all(
599+
tokenFiles.map(async (file) => {
600+
const tailwindOutPath = file.out.replace(
601+
'tokens.scss',
602+
'tokens-tailwind.css'
603+
)
604+
return makeDesignTokenTailwindCSS(
605+
file.out,
606+
tailwindOutPath,
607+
file.prefix
608+
)
609+
})
610+
)
531611
}

0 commit comments

Comments
 (0)