diff --git a/packages/theme/lib/tokens/color.js b/packages/theme/lib/tokens/color.js
deleted file mode 100644
index c28ef6ddb1..0000000000
--- a/packages/theme/lib/tokens/color.js
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * @file Color tokens — semantic surface, text, state, and brand colors.
- *
- * Light values are declared via `variable()`. Dark values are declared inside
- * the `dark` theme block so they emit under `html[data-theme="dark"]`.
- *
- * Link tokens are wired through `ref()` so brand overrides to `color.primary`
- * cascade to links without separate overrides. The dark theme intentionally
- * re-points links to their own oklab values rather than chaining through
- * primary because dark mode links need higher luminance than primary buttons.
- *
- * `warning` and `danger` deliberately stay on light values in dark mode — state
- * colors keep consistent intensity across themes so warnings read as urgent.
- */
-
-import { ref, theme, variable } from "../shared.js";
-
-export const colorAccent = variable("color.accent", "#fd4b2d");
-export const colorPrimary = variable("color.primary", "oklab(0.522 -0.0434 -0.1717)");
-export const colorPrimaryHover = variable("color.primary-hover", "oklab(0.3763 -0.0324 -0.1182)");
-export const colorText = variable("color.text", "oklab(0.1957 -0 0)");
-export const colorTextMuted = variable("color.text-muted", "oklab(0.5364 -0.0026 -0.0089)");
-
-export const colorLink = variable("color.link", ref(colorPrimary));
-export const colorLinkHover = variable("color.link-hover", ref(colorPrimaryHover));
-export const colorLinkVisited = variable("color.link-visited", "oklab(0.3679 0.0535 -0.1797)");
-
-export const colorSurface = variable("color.surface", "oklab(1 -0 0)");
-export const colorSurfaceMuted = variable("color.surface-muted", "oklab(0.9551 -0 0)");
-export const colorSurfaceRaised = variable("color.surface-raised", "oklab(0.9851 -0 0)");
-
-export const colorBorder = variable("color.border", "#d2d2d2");
-export const colorBorderStrong = variable("color.border-strong", "#8a8d90");
-
-export const colorInfo = variable("color.info", "oklab(0.6689 -0.0608 -0.1513)");
-export const colorSuccess = variable("color.success", "oklab(0.5549 -0.1071 0.0852)");
-export const colorWarning = variable("color.warning", "oklab(0.7864 0.0298 0.1604)");
-export const colorDanger = variable("color.danger", "oklab(0.5331 0.1798 0.1034)");
-
-// Dark theme overrides. Surface values are pinned near PatternFly 4's
-// BackgroundColor--100 (#151515) and the legacy drawer surface (#18191a) — the
-// earlier oklab(0.23) value visibly brightened every PF-backed dark panel.
-theme("dark", (ctx) => {
- ctx.variable(colorText, "oklab(0.9067 -0 0)");
- ctx.variable(colorTextMuted, "oklab(0.7407 -0.0007 -0.0017)");
- ctx.variable(colorLink, "oklab(70.367% -0.07498 -0.139)");
- ctx.variable(colorLinkHover, "oklab(0.7706 -0.0485 -0.1012)");
- ctx.variable(colorLinkVisited, "oklab(0.7137 0.0522 -0.151)");
- ctx.variable(colorSurface, "oklab(0.183 -0 0)");
- ctx.variable(colorSurfaceMuted, "oklab(0.14 -0 0)");
- ctx.variable(colorSurfaceRaised, "oklab(0.225 -0 0)");
- ctx.variable(colorBorder, "oklab(0.3906 0.0001 -0.0052)");
- ctx.variable(colorBorderStrong, "oklab(0.4602 -0.0003 -0.0034)");
- ctx.variable(colorInfo, "oklab(0.7706 -0.0485 -0.1012)");
- ctx.variable(colorSuccess, "oklab(0.6488 -0.1066 0.0846)");
-});
diff --git a/packages/theme/package-lock.json b/packages/theme/package-lock.json
index 964f81a723..af0e29dacf 100644
--- a/packages/theme/package-lock.json
+++ b/packages/theme/package-lock.json
@@ -21,7 +21,10 @@
"@goauthentik/prettier-config": "../prettier-config",
"@goauthentik/tsconfig": "../tsconfig",
"@styleframe/cli": "^4.0.0",
+ "@types/culori": "^4.0.1",
"@types/node": "^25.7.0",
+ "@typescript/native-preview": "^7.0.0-dev.20260616.1",
+ "culori": "^4.0.2",
"eslint": "^9.39.3",
"pino": "^10.3.1",
"pino-pretty": "^13.1.2",
@@ -638,6 +641,13 @@
"@styleframe/license": "^2.0.2"
}
},
+ "node_modules/@types/culori": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@types/culori/-/culori-4.0.1.tgz",
+ "integrity": "sha512-43M51r/22CjhbOXyGT361GZ9vncSVQ39u62x5eJdBQFviI8zWp2X5jzqg7k4M6PVgDQAClpy2bUe2dtwEgEDVQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/estree": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
@@ -944,6 +954,147 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/@typescript/native-preview": {
+ "version": "7.0.0-dev.20260616.1",
+ "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20260616.1.tgz",
+ "integrity": "sha512-+AuZUl7nkLPXL1rwsyZZF7iasu0HkrL5O6aWwUC0cObD5EvNgxz3hXbBhsSvuJ8tb2JRpVwFsE0BHPcYVe5GkA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsgo": "bin/tsgo.js"
+ },
+ "engines": {
+ "node": ">=16.20.0"
+ },
+ "optionalDependencies": {
+ "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260616.1",
+ "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260616.1",
+ "@typescript/native-preview-linux-arm": "7.0.0-dev.20260616.1",
+ "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260616.1",
+ "@typescript/native-preview-linux-x64": "7.0.0-dev.20260616.1",
+ "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260616.1",
+ "@typescript/native-preview-win32-x64": "7.0.0-dev.20260616.1"
+ }
+ },
+ "node_modules/@typescript/native-preview-darwin-arm64": {
+ "version": "7.0.0-dev.20260616.1",
+ "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20260616.1.tgz",
+ "integrity": "sha512-9SwegwwE7fYutnZJjTi2PeUXhKGFg82MGjSpCFD2cj5v9YQcs5oO5QsmeeMit4fMG8Z83Jy8knOF97d9NaQ7Bg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/native-preview-darwin-x64": {
+ "version": "7.0.0-dev.20260616.1",
+ "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20260616.1.tgz",
+ "integrity": "sha512-gl7R8OiwEBNxzs5wjbM9XOibTs5b6/gAgu0+En9pLpYuWR/EITs8Eh0mNQui4JYTp1SkoHvn09aRZKTQf21XTg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/native-preview-linux-arm": {
+ "version": "7.0.0-dev.20260616.1",
+ "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20260616.1.tgz",
+ "integrity": "sha512-QWXQS2CrhSpXbng7vBtCVDszFgwVBuJU8MCFhxZL0hH6s+XjQDSNNkGO1oHueBr+7HbSkkyqfNYVNXvgVMUJLQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/native-preview-linux-arm64": {
+ "version": "7.0.0-dev.20260616.1",
+ "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20260616.1.tgz",
+ "integrity": "sha512-CJjplWoE+EeYtbyNeP4fyuh0QcPiQBZJSLqPS07E3ugo3d9M/IG8WnL+3GFQ0g1p4c6QC/+OC0oTTQnGcNdd2Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/native-preview-linux-x64": {
+ "version": "7.0.0-dev.20260616.1",
+ "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20260616.1.tgz",
+ "integrity": "sha512-UVySBNnGTAnul2kO2/EhlVZl0CeTDcd6Lzi+WkvgyCgapTcU0BX1selQ4MEPtbVWKUbt9HBhxNku2dyaCcmKVw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/native-preview-win32-arm64": {
+ "version": "7.0.0-dev.20260616.1",
+ "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20260616.1.tgz",
+ "integrity": "sha512-kdX0QcDXiESH0o5DFdSWw15Hth0EtQobT9tX28ofqBUwGU1FFhwTKzA6qo7chaYUSW5MMd6XIME9rBwk+WizLw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/native-preview-win32-x64": {
+ "version": "7.0.0-dev.20260616.1",
+ "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20260616.1.tgz",
+ "integrity": "sha512-EB0Pj/0+nXifS3+wN0HdR1mKu7IieSpjMXoDjdXtJAdiPGlmKazBgHj97qn6CBvXisgLx0Eyb7tLCEQNDG53cA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
"node_modules/acorn": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
diff --git a/packages/theme/package.json b/packages/theme/package.json
index b94300a79c..8062dd67b6 100644
--- a/packages/theme/package.json
+++ b/packages/theme/package.json
@@ -55,11 +55,14 @@
},
"devDependencies": {
"@eslint/js": "^9.39.3",
- "@goauthentik/prettier-config": "../prettier-config",
"@goauthentik/eslint-config": "../eslint-config",
+ "@goauthentik/prettier-config": "../prettier-config",
"@goauthentik/tsconfig": "../tsconfig",
"@styleframe/cli": "^4.0.0",
+ "@types/culori": "^4.0.1",
"@types/node": "^25.7.0",
+ "@typescript/native-preview": "^7.0.0-dev.20260616.1",
+ "culori": "^4.0.2",
"eslint": "^9.39.3",
"pino": "^10.3.1",
"pino-pretty": "^13.1.2",
diff --git a/packages/theme/scripts/build.mjs b/packages/theme/scripts/build.mjs
deleted file mode 100644
index ac50a82e88..0000000000
--- a/packages/theme/scripts/build.mjs
+++ /dev/null
@@ -1,212 +0,0 @@
-/**
- * @file Build script for `@goauthentik/theme`.
- *
- * Runs the styleframe transpiler against the configured token tree, then
- * fans out the single emitted CSS string into one file per category
- * (color, typography, spacing, shape, shadow, motion, z-index) and a top-
- * level `index.css` that `@import`s them in deterministic order.
- *
- * Consumers can import the whole surface
- * (`@import "@goauthentik/theme/index.css"`) or cherry-pick a category
- * (`@import "@goauthentik/theme/color.css"`).
- *
- * Invoked by `npm run build:assets` and chained from `npm run build`.
- */
-
-import { mkdir, writeFile } from "node:fs/promises";
-import { dirname, resolve } from "node:path";
-import { fileURLToPath } from "node:url";
-
-import { build } from "../lib/node.js";
-
-const __dirname = dirname(fileURLToPath(import.meta.url));
-const PACKAGE_ROOT = resolve(__dirname, "..");
-const OUT_DIR = resolve(PACKAGE_ROOT, "dist");
-
-/**
- * @typedef {object} Category
- * @property {string} name Slug used for the output filename.
- * @property {string[]} prefixes
- * Token-name prefixes (after the `--ak-` strip) that belong to this category.
- */
-
-/** @type {Category[]} */
-const CATEGORIES = [
- { name: "color", prefixes: ["color-"] },
- {
- name: "typography",
- prefixes: ["font-family-", "font-size-", "font-weight-", "line-height-"],
- },
- { name: "spacing", prefixes: ["space-"] },
- { name: "shape", prefixes: ["radius-", "border-width-"] },
- { name: "shadow", prefixes: ["shadow-"] },
- { name: "motion", prefixes: ["duration-", "easing-"] },
- { name: "z-index", prefixes: ["z-index-"] },
-];
-
-const HEADER = [
- "/*",
- " * ⚠️ GENERATED FILE — do not edit directly.",
- " *",
- " * Source: packages/theme/lib/tokens/*.js",
- " * Built: packages/theme/scripts/build.mjs",
- " */",
- "",
-].join("\n");
-
-/**
- * One emitted block within the styleframe CSS output. `header` is the line
- * that opens the block (`:root {`, `@media (...) {`, `html[data-theme="..."] {`).
- * `prefix` is whitespace that should precede declarations inside the block,
- * preserved verbatim from the source. `closer` is the closing brace(s).
- *
- * @typedef {object} ParsedBlock
- * @property {string} header
- * @property {string[]} declarations
- * @property {string} closer
- */
-
-/**
- * Split the styleframe CSS output into top-level blocks. Each block is one of:
- *
- * :root { … }
- * html[data-theme="…"] { … }
- * @media (…) { :root { … } }
- *
- * Nested `:root` inside `@media` is preserved as part of the block — the
- * inner declarations are kept as a flat list and re-wrapped on emit.
- *
- * @param {string} css
- * @returns {ParsedBlock[]}
- */
-function parseBlocks(css) {
- /** @type {ParsedBlock[]} */
- const blocks = [];
-
- // Top-level blocks separated by blank lines in styleframe's output.
- // Each block ends with a balanced closing brace at column zero. We don't
- // need a real CSS parser — the styleframe emitter is regular enough that
- // a small line-based state machine handles it.
- const lines = css.split("\n");
- let i = 0;
-
- while (i < lines.length) {
- while (i < lines.length && (lines[i] ?? "").trim() === "") i++;
- if (i >= lines.length) break;
-
- const opener = lines[i] ?? "";
- const header = opener.trim();
- if (!header.endsWith("{")) {
- i++;
- continue;
- }
-
- const openerIndent = opener.match(/^\s*/)?.[0] ?? "";
- i++;
- /** @type {string[]} */
- const innerLines = [];
- while (i < lines.length) {
- const line = lines[i] ?? "";
- const trimmed = line.trim();
- const indent = line.match(/^\s*/)?.[0] ?? "";
- if (trimmed === "}" && indent === openerIndent) {
- i++;
- break;
- }
- innerLines.push(line);
- i++;
- }
-
- blocks.push({
- header,
- declarations: innerLines,
- closer: "}",
- });
- }
-
- return blocks;
-}
-
-/**
- * Extract every `--ak-*: …;` declaration from a flat list of lines (which may
- * include a nested `:root { … }` wrapper from an `@media` block).
- *
- * @param {string[]} lines
- * @returns {string[]}
- */
-function flattenDeclarations(lines) {
- return lines.filter((line) => /^\s*--ak-[a-z0-9-]+\s*:/.test(line));
-}
-
-/**
- * Build the CSS for one category by filtering each parsed block to that
- * category's declarations and re-wrapping them.
- *
- * @param {Category} category
- * @param {ParsedBlock[]} blocks
- */
-function buildCategoryFile(category, blocks) {
- /** @param {string} line */
- const matches = (line) => {
- const declaration = line.match(/--ak-([a-z0-9-]+):/);
- if (!declaration || !declaration[1]) return false;
- const name = declaration[1];
- return category.prefixes.some((prefix) => name.startsWith(prefix));
- };
-
- /** @type {string[]} */
- const sections = [];
-
- for (const block of blocks) {
- if (block.header.startsWith("@media")) {
- // Drill into the inner `:root { … }` wrapper.
- const inner = flattenDeclarations(block.declarations).filter(matches);
- if (inner.length === 0) continue;
- sections.push(
- `${block.header}\n\t:root {\n${inner
- .map((line) => "\t\t" + line.trim())
- .join("\n")}\n\t}\n}`,
- );
- continue;
- }
-
- const filtered = block.declarations.filter(matches);
- if (filtered.length === 0) continue;
- sections.push(
- `${block.header}\n${filtered.map((line) => "\t" + line.trim()).join("\n")}\n}`,
- );
- }
-
- if (sections.length === 0) return null;
- return HEADER + sections.join("\n\n") + "\n";
-}
-
-/**
- * Build the index.css that `@import`s every emitted category file in order.
- *
- * @param {string[]} emittedNames
- */
-function buildIndex(emittedNames) {
- const imports = emittedNames.map((name) => `@import "./${name}.css";`).join("\n");
- return HEADER + imports + "\n";
-}
-
-await mkdir(OUT_DIR, { recursive: true });
-
-const { css } = await build();
-const blocks = parseBlocks(css);
-
-const emitted = [];
-for (const category of CATEGORIES) {
- const content = buildCategoryFile(category, blocks);
- if (content === null) {
- console.warn(`⚠️ No declarations found for category ${category.name}; skipping.`);
- continue;
- }
- await writeFile(resolve(OUT_DIR, `${category.name}.css`), content, "utf-8");
- emitted.push(category.name);
-}
-
-await writeFile(resolve(OUT_DIR, "index.css"), buildIndex(emitted), "utf-8");
-
-console.log(`✅ Wrote dist/index.css + ${emitted.length} category files (${emitted.join(", ")})`);
diff --git a/packages/theme/index.js b/packages/theme/src/index.ts
similarity index 77%
rename from packages/theme/index.js
rename to packages/theme/src/index.ts
index ed99681683..ff0a055b22 100644
--- a/packages/theme/index.js
+++ b/packages/theme/src/index.ts
@@ -9,5 +9,5 @@
* filesystem live in `./lib/node.js` (exposed via the `./build` subpath).
*/
-export { instance, ref, selector, theme, variable } from "./lib/shared.js";
-export * from "./lib/tokens/index.js";
+export { instance, ref, selector, theme, variable } from "./shared.js";
+export * from "./tokens/index.js";
diff --git a/packages/theme/lib/node.js b/packages/theme/src/node.ts
similarity index 87%
rename from packages/theme/lib/node.js
rename to packages/theme/src/node.ts
index f22884f787..3168715467 100644
--- a/packages/theme/lib/node.js
+++ b/packages/theme/src/node.ts
@@ -1,4 +1,4 @@
-/**
+/*
* @file Node-only build helpers for `@goauthentik/theme`.
*
* Re-exports everything the browser entry exports, plus filesystem-touching
@@ -7,7 +7,8 @@
* @import { OutputFile } from "@styleframe/transpiler";
*/
-///
+
+import type { OutputFile } from "@styleframe/transpiler";
import { writeFile } from "node:fs/promises";
@@ -28,6 +29,10 @@ export * from "./tokens/index.js";
* this absolute path in addition to being returned.
*/
+interface BuildOptions {
+ outFile?: string;
+}
+
/**
* @typedef {object} BuildResult
* @property {string} css The full CSS string emitted by styleframe.
@@ -35,6 +40,11 @@ export * from "./tokens/index.js";
* to inspect every file styleframe produced.
*/
+interface BuildResult {
+ css: string;
+ files: OutputFile[];
+}
+
/**
* Transpile the configured token tree to CSS.
*
@@ -44,7 +54,7 @@ export * from "./tokens/index.js";
* @param {BuildOptions} [options]
* @returns {Promise}
*/
-export async function build(options = {}) {
+export async function build(options: BuildOptions = {}): Promise {
const output = await transpile(instance, { type: "css" });
const cssFile = output.files.find((file) => file.name.endsWith(".css"));
const css = cssFile?.content ?? "";
diff --git a/packages/theme/lib/shared.js b/packages/theme/src/shared.ts
similarity index 89%
rename from packages/theme/lib/shared.js
rename to packages/theme/src/shared.ts
index 6c80be7d59..cadf95fa2f 100644
--- a/packages/theme/lib/shared.js
+++ b/packages/theme/src/shared.ts
@@ -25,11 +25,12 @@ import { styleframe } from "styleframe";
* @type {StyleframeOptions}
*/
export const authentikStyleframeOptions = {
+ indent: " ",
variables: {
- name: ({ name }) => "ak-" + name.replace(/\./g, "-"),
+ name: ({ name }: { name: string }) => `ak-${name.replace(/\./g, "-")}`
},
themes: {
- selector: ({ name }) => `html[data-theme="${name}"]`,
+ selector: ({ name }: { name: string}) => `html[data-theme="${name}"]`,
},
};
diff --git a/packages/theme/src/tokens/color.ts b/packages/theme/src/tokens/color.ts
new file mode 100644
index 0000000000..73fc198b9c
--- /dev/null
+++ b/packages/theme/src/tokens/color.ts
@@ -0,0 +1,106 @@
+/**
+ * @file Color tokens — semantic surface, text, state, and brand colors.
+ *
+ * Light values are declared via `variable()`. Dark values are declared inside
+ * the `dark` theme block so they emit under `html[data-theme="dark"]`.
+ *
+ * Link tokens are wired through `ref()` so brand overrides to `color.primary`
+ * cascade to links without separate overrides. The dark theme intentionally
+ * re-points links to their own oklab values rather than chaining through
+ * primary because dark mode links need higher luminance than primary buttons.
+ *
+ * `warning` and `danger` deliberately stay on light values in dark mode — state
+ * colors keep consistent intensity across themes so warnings read as urgent.
+ */
+
+import { instance, theme } from "../shared.js";
+
+import { createVariableFunction } from "styleframe";
+import { createUseVariable } from "@styleframe/theme";
+import { formatHex, oklch as toOklch, toGamut } from "culori";
+
+/*
+ * Restrict the OKLCH values to six decimal places; Javascript will give it to you accurate to 16
+ * places, but OKLCH doesn't much care past six, and 16 is just unreadable.
+ *
+ * Includes in each OKLCH output line a comment containing the RGB value, to help IDEs show the
+ * color accurately.
+ */
+
+
+const toSrgb = toGamut("rgb", "oklch");
+const round = (v: number, precision = 6) => Number(v.toFixed(precision)).toString();
+const oklchTransform = (value: string) => {
+ const c = toOklch(value);
+ if (!c || c.l == null || c.c == null) {
+ return value;
+ }
+
+ const result = `oklch(${round(c.l)} ${round(c.c)} ${round(c.h ?? 0)} / ${round(c.alpha ?? 1)})`;
+ return `${result} /* ${formatHex(toSrgb(c))} */`;
+};
+const useColorDesignTokens = createUseVariable("color", { transform: oklchTransform });
+
+export const {
+ colorAccent,
+ colorPrimary,
+ colorPrimaryHover,
+ colorText,
+ colorTextMuted,
+ colorLink,
+ colorLinkHover,
+ colorLinkVisited,
+ colorSurface,
+ colorSurfaceRaised,
+ colorSurfaceMuted,
+ colorBorder,
+ colorBorderStrong,
+ colorInfo,
+ colorSuccess,
+ colorWarning,
+ colorDanger,
+} = useColorDesignTokens(instance, {
+ "accent": "#fd4b2d",
+ "primary": "#0066cc",
+ "primary-hover": "#004080",
+ "text": "#151515",
+ "text-muted": "#6a6e73",
+ "link": "@color.primary",
+ "link-hover": "@color.primary-hover",
+ "link-visited": "#40199a",
+ "surface": "#ffffff",
+ "surface-raised": "#fafafa",
+ "surface-muted": "#f0f0f0",
+ "border": "#d2d2d2",
+ "border-strong": "#8a8d90",
+ "info": "#2b9af3",
+ "success": "#3e8635",
+ "warning": "#f0ab00",
+ "danger": "#c9190b",
+});
+
+type Variable = ReturnType>;
+type VPPair = [Variable, string];
+
+// Dark theme overrides. Surface values are pinned near PatternFly 4's
+// BackgroundColor--100 (#151515) and the legacy drawer surface (#18191a) — the
+// earlier oklab(0.23) value visibly brightened every PF-backed dark panel.
+theme("dark", (ctx) => {
+
+ const darkColors: VPPair[] = [
+ [colorText, "#e0e0e0"],
+ [colorTextMuted, "#aaabac"],
+ [colorLink, "#20a9f8"],
+ [colorLinkHover, "#73bcf7"],
+ [colorLinkVisited, "#a18fff"],
+ [colorSurface, "#121212"],
+ [colorSurfaceMuted, "#090909"],
+ [colorSurfaceRaised, "#1c1c1c"],
+ [colorBorder, "#444548"],
+ [colorBorderStrong, "#57585a"],
+ [colorInfo, "#73bcf7"],
+ [colorSuccess, "#5ba352"]
+ ];
+
+ darkColors.forEach(([v, p]) => ctx.variable(v, oklchTransform(p)));
+});
diff --git a/packages/theme/lib/tokens/index.js b/packages/theme/src/tokens/index.ts
similarity index 100%
rename from packages/theme/lib/tokens/index.js
rename to packages/theme/src/tokens/index.ts
diff --git a/packages/theme/lib/tokens/motion.js b/packages/theme/src/tokens/motion.ts
similarity index 100%
rename from packages/theme/lib/tokens/motion.js
rename to packages/theme/src/tokens/motion.ts
diff --git a/packages/theme/lib/tokens/shadow.js b/packages/theme/src/tokens/shadow.ts
similarity index 100%
rename from packages/theme/lib/tokens/shadow.js
rename to packages/theme/src/tokens/shadow.ts
diff --git a/packages/theme/lib/tokens/shape.js b/packages/theme/src/tokens/shape.ts
similarity index 100%
rename from packages/theme/lib/tokens/shape.js
rename to packages/theme/src/tokens/shape.ts
diff --git a/packages/theme/lib/tokens/spacing.js b/packages/theme/src/tokens/spacing.ts
similarity index 100%
rename from packages/theme/lib/tokens/spacing.js
rename to packages/theme/src/tokens/spacing.ts
diff --git a/packages/theme/lib/tokens/typography.js b/packages/theme/src/tokens/typography.ts
similarity index 100%
rename from packages/theme/lib/tokens/typography.js
rename to packages/theme/src/tokens/typography.ts
diff --git a/packages/theme/lib/tokens/z-index.js b/packages/theme/src/tokens/z-index.ts
similarity index 100%
rename from packages/theme/lib/tokens/z-index.js
rename to packages/theme/src/tokens/z-index.ts
diff --git a/packages/theme/styleframe.config.mjs b/packages/theme/styleframe.config.ts
similarity index 88%
rename from packages/theme/styleframe.config.mjs
rename to packages/theme/styleframe.config.ts
index 568da47c0c..2a9664d16f 100644
--- a/packages/theme/styleframe.config.mjs
+++ b/packages/theme/styleframe.config.ts
@@ -11,8 +11,8 @@
* the same tree the rest of the package uses.
*/
-import "./lib/tokens/index.js";
+import "./src/tokens/index.js";
-import { instance } from "./lib/shared.js";
+import { instance } from "./src/shared.js";
export default instance;
diff --git a/packages/theme/types/node.d.ts b/packages/theme/types/node.d.ts
deleted file mode 100644
index c118164b66..0000000000
--- a/packages/theme/types/node.d.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-declare module "process" {
- import { Level } from "pino";
-
- global {
- namespace NodeJS {
- interface ProcessEnv {
- /**
- * An environment variable used to determine
- * whether Node.js is running in production mode.
- *
- * @see {@link https://nodejs.org/en/learn/getting-started/nodejs-the-difference-between-development-and-production | The difference between development and production}
- */
- readonly NODE_ENV?: "development" | "production";
-
- /**
- * Whether or not we are running on a CI server.
- */
- readonly CI?: string;
-
- /**
- * The application log level.
- */
- readonly AK_LOG_LEVEL?: Level;
- }
- }
- }
-}