mirror of
https://github.com/navikt/aksel.git
synced 2026-10-12 16:52:03 +00:00
feat(tailwind): Tw v4 config (#5036)
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
This commit is contained in:
1 parent
69f137e01e
commit
e6dd922d8c
10 files changed
+220
-9
No files matched your search
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@navikt/ds-tailwind": minor
|
||||
---
|
||||
|
||||
Tailwind v4 config now available at `@navikt/ds-tailwind/v4`
|
||||
@@ -1,3 +1,4 @@
|
||||
src/tokens.ts
|
||||
tailwind.config.js
|
||||
tailwind.darkside.config.js
|
||||
tailwind.darkside.config.js
|
||||
tailwind4.css
|
||||
@@ -9,7 +9,7 @@ Custom tailwind-preset based on `@navikt/ds-tokens`.
|
||||
npm install @navikt/ds-tailwind
|
||||
```
|
||||
|
||||
## Usage
|
||||
## Use with tailwind v3
|
||||
|
||||
```javascript
|
||||
// tailwind.config.js
|
||||
@@ -19,10 +19,16 @@ module.exports = {
|
||||
};
|
||||
```
|
||||
|
||||
## Use with tailwind v4
|
||||
|
||||
```css
|
||||
@import "tailwindcss";
|
||||
@import "@navikt/ds-tailwind/v4";
|
||||
```
|
||||
|
||||
## Learn more
|
||||
|
||||
- [Docs](https://aksel.nav.no/komponenter)
|
||||
- [Storybook](https://aksel.nav.no/storybook/)
|
||||
- [Docs](https://aksel.nav.no/grunnleggende/kode/tailwind)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -10,10 +10,11 @@
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"tailwind.config.js",
|
||||
"tailwind.config.d.ts"
|
||||
"tailwind.config.d.ts",
|
||||
"tailwind4.css"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "yarn tsx ./src/build.ts",
|
||||
"build": "yarn tsx ./src/build-v3.ts && yarn tsx ./src/build-v4.ts",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest watch"
|
||||
},
|
||||
@@ -28,6 +29,7 @@
|
||||
"homepage": "https://aksel.nav.no/grunnleggende/kode/tailwind/",
|
||||
"devDependencies": {
|
||||
"@navikt/ds-tokens": "^8.14.0",
|
||||
"@types/node": "^24.10.9",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tsx": "^4.20.6",
|
||||
"typescript": "6.0.3",
|
||||
@@ -50,6 +52,7 @@
|
||||
"default": "./tailwind.config.js"
|
||||
}
|
||||
},
|
||||
"./v4": "./tailwind4.css",
|
||||
"./package.json": "./package.json"
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,115 @@
|
||||
import { writeFileSync } from "node:fs";
|
||||
import * as TokensBuild from "@navikt/ds-tokens/js";
|
||||
import { kebabCaseForAlpha } from "../../tokens/config/kebabCase";
|
||||
import { breakpointTokenConfig } from "../../tokens/src/tokens/breakpoint";
|
||||
|
||||
const transformedTokens = Object.fromEntries(
|
||||
Object.entries(TokensBuild)
|
||||
.map(([key, value]) => {
|
||||
return [kebabCaseForAlpha(key), value];
|
||||
})
|
||||
.filter(([key]) => key !== "default"),
|
||||
);
|
||||
|
||||
const nonColorTokens = [
|
||||
"space",
|
||||
"shadow",
|
||||
"font-weight",
|
||||
"font-size",
|
||||
"font-line-height",
|
||||
"font-family",
|
||||
"border-radius",
|
||||
"breakpoint",
|
||||
"opacity",
|
||||
"radius",
|
||||
];
|
||||
|
||||
const colorTokensEntries = Object.entries(transformedTokens).filter(([key]) => {
|
||||
return !nonColorTokens.find((prefix) => key.toLowerCase().includes(prefix));
|
||||
});
|
||||
const colors = Object.fromEntries(colorTokensEntries) as Record<string, string>;
|
||||
|
||||
export const v4Config = {
|
||||
colors,
|
||||
shadows: extractTokensForCategory("shadow"),
|
||||
fontSizes: extractTokensForCategory("font-size"),
|
||||
fontWeights: extractTokensForCategory("font-weight"),
|
||||
lineHeights: extractTokensForCategory("font-line-height"),
|
||||
fontFamilies: extractTokensForCategory("font-family"),
|
||||
opacity: extractTokensForCategory("opacity"),
|
||||
radius: extractTokensForCategory("radius"),
|
||||
breakpoints: {
|
||||
sm: breakpointTokenConfig.breakpoint.sm.value,
|
||||
md: breakpointTokenConfig.breakpoint.md.value,
|
||||
lg: breakpointTokenConfig.breakpoint.lg.value,
|
||||
xl: breakpointTokenConfig.breakpoint.xl.value,
|
||||
"2xl": breakpointTokenConfig.breakpoint["2xl"].value,
|
||||
},
|
||||
};
|
||||
|
||||
function generateThemeCSS(): string {
|
||||
const lines: string[] = ["@theme {"];
|
||||
|
||||
for (const [key, value] of Object.entries(v4Config.colors)) {
|
||||
lines.push(` --color-ax-${key}: ${value};`);
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
for (const [key, value] of Object.entries(v4Config.shadows)) {
|
||||
lines.push(` --shadow-ax-${key}: ${value};`);
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
for (const [key, value] of Object.entries(v4Config.fontSizes)) {
|
||||
lines.push(` --font-size-ax-${key}: ${value};`);
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
for (const [key, value] of Object.entries(v4Config.fontWeights)) {
|
||||
lines.push(` --font-weight-ax-${key}: ${value};`);
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
for (const [key, value] of Object.entries(v4Config.lineHeights)) {
|
||||
lines.push(` --leading-ax-${key}: ${value};`);
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
for (const [key, value] of Object.entries(v4Config.fontFamilies)) {
|
||||
lines.push(` --font-family-ax-${key}: ${value};`);
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
for (const [key, value] of Object.entries(v4Config.opacity)) {
|
||||
lines.push(` --opacity-ax-${key}: ${value};`);
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
for (const [key, value] of Object.entries(v4Config.radius)) {
|
||||
lines.push(` --radius-ax-${key}: ${value};`);
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
for (const [key, value] of Object.entries(v4Config.breakpoints)) {
|
||||
lines.push(` --breakpoint-ax-${key}: ${value};`);
|
||||
}
|
||||
|
||||
lines.push("}");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
writeFileSync("tailwind4.css", generateThemeCSS());
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Utilities */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
function extractTokensForCategory(tokenName: string) {
|
||||
const tokens = Object.entries(transformedTokens)
|
||||
.filter(([key]) => key.startsWith(tokenName))
|
||||
.map(([key, value]) => [key.replace(`${tokenName}-`, ""), value]);
|
||||
|
||||
return Object.fromEntries(tokens);
|
||||
}
|
||||
|
||||
export { generateThemeCSS };
|
||||
@@ -1,7 +1,8 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { config } from "./build";
|
||||
import { config } from "./build-v3";
|
||||
import { generateThemeCSS, v4Config } from "./build-v4";
|
||||
|
||||
describe("Tailwind config", () => {
|
||||
describe("Tailwind v3 config", () => {
|
||||
test("should have correct color tokens", () => {
|
||||
const colorKeys = Object.keys(config.theme.colors);
|
||||
expect(colorKeys).not.toContain("spacing");
|
||||
@@ -14,3 +15,80 @@ describe("Tailwind config", () => {
|
||||
expect(colorKeys).not.toContain("breakpoint");
|
||||
});
|
||||
});
|
||||
|
||||
const allowedVariables = [
|
||||
"--color-",
|
||||
"--font-",
|
||||
"--text-",
|
||||
"--font-weight-",
|
||||
"--tracking-",
|
||||
"--leading-",
|
||||
"--tab-size-",
|
||||
"--breakpoint-",
|
||||
"--container-",
|
||||
"--spacing-",
|
||||
"--radius-",
|
||||
"--shadow-",
|
||||
"--inset-shadow-",
|
||||
"--drop-shadow-",
|
||||
"--blur-",
|
||||
"--perspective-",
|
||||
"--zoom-",
|
||||
"--aspect-",
|
||||
"--ease-",
|
||||
"--animate-",
|
||||
"--opacity-",
|
||||
];
|
||||
|
||||
describe("Tailwind v4 config", () => {
|
||||
test("should have correct color tokens without non-color categories", () => {
|
||||
const colorKeys = Object.keys(v4Config.colors);
|
||||
expect(colorKeys).not.toContain("spacing");
|
||||
expect(colorKeys).not.toContain("shadow");
|
||||
expect(colorKeys).not.toContain("font-weight");
|
||||
expect(colorKeys).not.toContain("font-size");
|
||||
expect(colorKeys).not.toContain("font-line-height");
|
||||
expect(colorKeys).not.toContain("font-family");
|
||||
expect(colorKeys).not.toContain("border-radius");
|
||||
expect(colorKeys).not.toContain("breakpoint");
|
||||
expect(colorKeys.some((k) => k.includes("radius"))).toBe(false);
|
||||
});
|
||||
|
||||
test("should have shadow tokens", () => {
|
||||
expect(Object.keys(v4Config.shadows).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("should have font-size tokens", () => {
|
||||
expect(Object.keys(v4Config.fontSizes).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("should have breakpoints", () => {
|
||||
expect(v4Config.breakpoints).toEqual({
|
||||
sm: "480px",
|
||||
md: "768px",
|
||||
lg: "1024px",
|
||||
xl: "1280px",
|
||||
"2xl": "1440px",
|
||||
});
|
||||
});
|
||||
|
||||
test("should have radius tokens", () => {
|
||||
expect(Object.keys(v4Config.radius).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("all generated variables should use allowed Tailwind v4 theme namespaces", () => {
|
||||
const css = generateThemeCSS();
|
||||
const variableLines = css
|
||||
.split("\n")
|
||||
.filter((line) => line.trim().startsWith("--"))
|
||||
.map((line) => line.trim().split(":")[0]);
|
||||
|
||||
expect(variableLines.length).toBeGreaterThan(0);
|
||||
|
||||
const invalid = variableLines.filter(
|
||||
(v) => !allowedVariables.some((prefix) => v.startsWith(prefix)),
|
||||
);
|
||||
|
||||
expect(invalid).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,8 @@
|
||||
"esModuleInterop": true,
|
||||
"allowJs": true,
|
||||
"moduleResolution": "bundler",
|
||||
"module": "preserve"
|
||||
"module": "preserve",
|
||||
"types": ["@types/node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
@@ -85,5 +85,6 @@ export default {
|
||||
"**/@navikt/aksel/**",
|
||||
"**/examples/**",
|
||||
"plugin.js",
|
||||
"**/tailwind4.css",
|
||||
],
|
||||
};
|
||||
Reference in new issue
Block a user