mirror of
https://github.com/goauthentik/authentik.git
synced 2026-06-17 19:09:11 +03:00
Just the theme, ma'am.
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
README.md
|
||||
node_modules
|
||||
_media
|
||||
!.github/README.md
|
||||
@@ -0,0 +1,13 @@
|
||||
# Prettier Ignorefile
|
||||
|
||||
## Node
|
||||
node_modules
|
||||
|
||||
## Static Files
|
||||
**/LICENSE
|
||||
./README.md
|
||||
|
||||
## Build asset directories
|
||||
coverage
|
||||
dist
|
||||
out
|
||||
@@ -0,0 +1,18 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2026 Authentik Security, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
||||
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
||||
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
|
||||
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @file ESLint Configuration
|
||||
*
|
||||
* @import { Config } from "eslint/config";
|
||||
*/
|
||||
|
||||
import { createESLintPackageConfig } from "@goauthentik/eslint-config";
|
||||
|
||||
import { defineConfig } from "eslint/config";
|
||||
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @type {Config[]}
|
||||
*/
|
||||
const eslintConfig = defineConfig(
|
||||
createESLintPackageConfig({
|
||||
parserOptions: {
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
}),
|
||||
{
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
},
|
||||
files: ["shared/**"],
|
||||
},
|
||||
);
|
||||
|
||||
export default eslintConfig;
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @file Public entry point for `@goauthentik/theme`.
|
||||
*
|
||||
* Importing this module registers every token against the shared styleframe
|
||||
* instance and re-exports the handles + primitives consumers need to author
|
||||
* component styles or build CSS.
|
||||
*
|
||||
* Browser-safe: no Node-only dependencies. Build helpers that touch the
|
||||
* 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";
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* @file Node-only build helpers for `@goauthentik/theme`.
|
||||
*
|
||||
* Re-exports everything the browser entry exports, plus filesystem-touching
|
||||
* helpers that wrap styleframe's `transpile()` for use from build scripts.
|
||||
*
|
||||
* @import { OutputFile } from "@styleframe/transpiler";
|
||||
*/
|
||||
|
||||
/// <reference types="../types/node.js" />
|
||||
|
||||
import { writeFile } from "node:fs/promises";
|
||||
|
||||
import { instance } from "./shared.js";
|
||||
|
||||
import { transpile } from "@styleframe/transpiler";
|
||||
|
||||
// Re-export everything the browser entry exposes so consumers can do either
|
||||
// `import { variable } from "@goauthentik/theme"` (browser-safe) or
|
||||
// `import { build } from "@goauthentik/theme/build"` (Node-only) from the
|
||||
// same package without juggling subpaths.
|
||||
export * from "./shared.js";
|
||||
export * from "./tokens/index.js";
|
||||
|
||||
/**
|
||||
* @typedef {object} BuildOptions
|
||||
* @property {string} [outFile] If provided, the generated CSS is written to
|
||||
* this absolute path in addition to being returned.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} BuildResult
|
||||
* @property {string} css The full CSS string emitted by styleframe.
|
||||
* @property {OutputFile[]} files Raw transpile output for callers that want
|
||||
* to inspect every file styleframe produced.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Transpile the configured token tree to CSS.
|
||||
*
|
||||
* Equivalent to `npx styleframe build` for the css output type, but returns
|
||||
* the string directly so build scripts can post-process before writing.
|
||||
*
|
||||
* @param {BuildOptions} [options]
|
||||
* @returns {Promise<BuildResult>}
|
||||
*/
|
||||
export async function build(options = {}) {
|
||||
const output = await transpile(instance, { type: "css" });
|
||||
const cssFile = output.files.find((file) => file.name.endsWith(".css"));
|
||||
const css = cssFile?.content ?? "";
|
||||
|
||||
if (options.outFile) {
|
||||
await writeFile(options.outFile, css, "utf-8");
|
||||
}
|
||||
|
||||
return { css, files: output.files };
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* @file Styleframe instance and shared primitives for the authentik theme.
|
||||
*
|
||||
* This module configures one global {@link Styleframe} instance and re-exports
|
||||
* its primitives ({@link variable}, {@link theme}, {@link ref}, {@link selector},
|
||||
* etc.) for use by the per-category token modules under `./tokens/`.
|
||||
*
|
||||
* The instance is configured so that:
|
||||
*
|
||||
* - Variable names use the `--ak-*` prefix authentik components and brand custom
|
||||
* CSS expect. Source tokens are written in dot-notation (`color.primary`)
|
||||
* and the configured name function rewrites that to `ak-color-primary` before
|
||||
* styleframe prepends the leading `--`.
|
||||
* - The theme selector matches the existing `html[data-theme="..."]` convention
|
||||
* used across the authentik stylesheets.
|
||||
*
|
||||
* @import { Styleframe, StyleframeOptions } from "@styleframe/core";
|
||||
*/
|
||||
|
||||
import { styleframe } from "styleframe";
|
||||
|
||||
/**
|
||||
* Authentik-specific styleframe configuration.
|
||||
*
|
||||
* @type {StyleframeOptions}
|
||||
*/
|
||||
export const authentikStyleframeOptions = {
|
||||
variables: {
|
||||
name: ({ name }) => "ak-" + name.replace(/\./g, "-"),
|
||||
},
|
||||
themes: {
|
||||
selector: ({ name }) => `html[data-theme="${name}"]`,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Singleton styleframe instance used by every token module.
|
||||
*
|
||||
* Importing any module under `./tokens/` triggers the side-effects that
|
||||
* register variables and themes against this instance.
|
||||
*
|
||||
* @type {Styleframe}
|
||||
*/
|
||||
export const instance = styleframe(authentikStyleframeOptions);
|
||||
|
||||
export const {
|
||||
variable,
|
||||
theme,
|
||||
ref,
|
||||
selector,
|
||||
atRule,
|
||||
keyframes,
|
||||
media,
|
||||
css,
|
||||
utility,
|
||||
modifier,
|
||||
recipe,
|
||||
} = instance;
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* @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)");
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* @file Re-exports every public token handle.
|
||||
*
|
||||
* Importing this module ensures all token modules execute their side-effects
|
||||
* — registering variables and theme overrides against the singleton styleframe
|
||||
* instance in `../shared.js` — before any consumer reaches `transpile()`.
|
||||
*/
|
||||
|
||||
export * from "./color.js";
|
||||
export * from "./typography.js";
|
||||
export * from "./spacing.js";
|
||||
export * from "./shape.js";
|
||||
export * from "./shadow.js";
|
||||
export * from "./motion.js";
|
||||
export * from "./z-index.js";
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* @file Motion tokens — duration and easing.
|
||||
*
|
||||
* Reduced motion is expressed two ways. Both override `--ak-duration-normal`
|
||||
* to `0ms`:
|
||||
*
|
||||
* 1. `@media (prefers-reduced-motion: reduce)` — triggers automatically from
|
||||
* the operating system's accessibility preference. This is the path most
|
||||
* consumers actually hit.
|
||||
* 2. `html[data-theme="reduced"]` — opt-in via data attribute, for explicit
|
||||
* forced-reduced-motion or for previewing the reduced state. This block
|
||||
* exists primarily so the styleframe DTCG export can represent reduced
|
||||
* motion as a theme modifier — DTCG has no native concept for media
|
||||
* queries.
|
||||
*
|
||||
* The two emissions are independent and don't conflict; either one alone is
|
||||
* enough to zero the duration. Keeping both lets us roundtrip the reduced
|
||||
* state through DTCG-aware tooling without losing the automatic OS trigger.
|
||||
*/
|
||||
|
||||
import { media, theme, variable } from "../shared.js";
|
||||
|
||||
export const durationNormal = variable("duration.normal", "250ms");
|
||||
export const easingStandard = variable("easing.standard", "cubic-bezier(0.645, 0.045, 0.355, 1)");
|
||||
|
||||
media("(prefers-reduced-motion: reduce)", (ctx) => {
|
||||
ctx.selector(":root", (root) => {
|
||||
root.variable(durationNormal, "0ms");
|
||||
});
|
||||
});
|
||||
|
||||
theme("reduced", (ctx) => {
|
||||
ctx.variable(durationNormal, "0ms");
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @file Shadow tokens — sm..xl drop shadows and an inset variant.
|
||||
*
|
||||
* Dark theme uses higher opacity to read against the deeper backgrounds. The
|
||||
* inset shadow uses a near-solid color in dark mode rather than a faded rgba.
|
||||
*/
|
||||
|
||||
import { theme, variable } from "../shared.js";
|
||||
|
||||
export const shadowSm = variable(
|
||||
"shadow.sm",
|
||||
"0 0.0625rem 0.125rem 0 rgba(3, 3, 3, 0.12), 0 0 0.125rem 0 rgba(3, 3, 3, 0.06)",
|
||||
);
|
||||
export const shadowMd = variable(
|
||||
"shadow.md",
|
||||
"0 0.25rem 0.5rem 0rem rgba(3, 3, 3, 0.12), 0 0 0.25rem 0 rgba(3, 3, 3, 0.06)",
|
||||
);
|
||||
export const shadowLg = variable(
|
||||
"shadow.lg",
|
||||
"0 0.5rem 1rem 0 rgba(3, 3, 3, 0.16), 0 0 0.375rem 0 rgba(3, 3, 3, 0.08)",
|
||||
);
|
||||
export const shadowXl = variable(
|
||||
"shadow.xl",
|
||||
"0 0.75rem 1.5rem 0 rgba(3, 3, 3, 0.2), 0 0 0.5rem 0 rgba(3, 3, 3, 0.1)",
|
||||
);
|
||||
export const shadowInset = variable("shadow.inset", "inset 0 0 0.625rem 0 rgba(3, 3, 3, 0.25)");
|
||||
|
||||
theme("dark", (ctx) => {
|
||||
ctx.variable(
|
||||
shadowSm,
|
||||
"0 0.0625rem 0.125rem 0 rgba(3, 3, 3, 0.48), 0 0 0.125rem 0 rgba(3, 3, 3, 0.24)",
|
||||
);
|
||||
ctx.variable(
|
||||
shadowMd,
|
||||
"0 0.25rem 0.5rem 0rem rgba(3, 3, 3, 0.48), 0 0 0.25rem 0 rgba(3, 3, 3, 0.24)",
|
||||
);
|
||||
ctx.variable(
|
||||
shadowLg,
|
||||
"0 0.5rem 1rem 0 rgba(3, 3, 3, 0.64), 0 0 0.375rem 0 rgba(3, 3, 3, 0.32)",
|
||||
);
|
||||
ctx.variable(
|
||||
shadowXl,
|
||||
"0 0.75rem 1.5rem 0 rgba(3, 3, 3, 0.8), 0 0 0.5rem 0 rgba(3, 3, 3, 0.4)",
|
||||
);
|
||||
ctx.variable(shadowInset, "inset 0 0 0.625rem 0 #030303");
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* @file Shape tokens — border radii and stroke widths.
|
||||
*/
|
||||
|
||||
import { variable } from "../shared.js";
|
||||
|
||||
export const radiusSm = variable("radius.sm", "3px");
|
||||
export const radiusPill = variable("radius.pill", "30rem");
|
||||
|
||||
export const borderWidthSm = variable("border-width.sm", "1px");
|
||||
export const borderWidthMd = variable("border-width.md", "2px");
|
||||
export const borderWidthLg = variable("border-width.lg", "3px");
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* @file Spacing tokens — single scale from xs (4px) to 4xl (80px) at 16px base.
|
||||
*/
|
||||
|
||||
import { variable } from "../shared.js";
|
||||
|
||||
export const spaceXs = variable("space.xs", "0.25rem");
|
||||
export const spaceSm = variable("space.sm", "0.5rem");
|
||||
export const spaceMd = variable("space.md", "1rem");
|
||||
export const spaceLg = variable("space.lg", "1.5rem");
|
||||
export const spaceXl = variable("space.xl", "2rem");
|
||||
export const space2xl = variable("space.2xl", "3rem");
|
||||
export const space3xl = variable("space.3xl", "4rem");
|
||||
export const space4xl = variable("space.4xl", "5rem");
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @file Typography tokens — font families, sizes, line heights, weights.
|
||||
*
|
||||
* Font families fall through to generic CSS `var(...)` references defined
|
||||
* upstream by the existing `fonts.css`. Sizes follow a modular xs..4xl scale.
|
||||
* `semi-bold` is deliberately omitted from the public surface because
|
||||
* PatternFly's `--pf-global--FontWeight--semi-bold` collapses to 700 (same as
|
||||
* bold) unless the Overpass font scale is active.
|
||||
*/
|
||||
|
||||
import { variable } from "../shared.js";
|
||||
|
||||
export const fontFamilyBody = variable("font.family-body", "var(--ak-font-family-sans-serif)");
|
||||
export const fontFamilyHeading = variable("font.family-heading", "var(--ak-generic-display)");
|
||||
export const fontFamilyCode = variable("font.family-code", "var(--ak-font-family-monospace)");
|
||||
|
||||
export const fontSizeXs = variable("font.size-xs", "0.75rem");
|
||||
export const fontSizeSm = variable("font.size-sm", "0.875rem");
|
||||
export const fontSizeMd = variable("font.size-md", "1rem");
|
||||
export const fontSizeLg = variable("font.size-lg", "1.125rem");
|
||||
export const fontSizeXl = variable("font.size-xl", "1.25rem");
|
||||
export const fontSize2xl = variable("font.size-2xl", "1.5rem");
|
||||
export const fontSize3xl = variable("font.size-3xl", "1.75rem");
|
||||
export const fontSize4xl = variable("font.size-4xl", "2.25rem");
|
||||
|
||||
export const fontWeightLight = variable("font.weight-light", 300);
|
||||
export const fontWeightNormal = variable("font.weight-normal", 400);
|
||||
export const fontWeightBold = variable("font.weight-bold", 700);
|
||||
|
||||
export const lineHeightSm = variable("line-height.sm", 1.3);
|
||||
export const lineHeightMd = variable("line-height.md", 1.5);
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @file Z-index tokens — xs..2xl tiers matching PatternFly 4's scale.
|
||||
*
|
||||
* Spelled out as `z-index.*` rather than `z.*` so the emitted CSS
|
||||
* (`--ak-z-index-md`) is unambiguous in brand custom CSS and IDE
|
||||
* autocomplete.
|
||||
*/
|
||||
|
||||
import { variable } from "../shared.js";
|
||||
|
||||
export const zIndexXs = variable("z-index.xs", 100);
|
||||
export const zIndexSm = variable("z-index.sm", 200);
|
||||
export const zIndexMd = variable("z-index.md", 300);
|
||||
export const zIndexLg = variable("z-index.lg", 400);
|
||||
export const zIndexXl = variable("z-index.xl", 500);
|
||||
export const zIndex2xl = variable("z-index.2xl", 600);
|
||||
Generated
+2656
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"name": "@goauthentik/theme",
|
||||
"version": "1.0.0",
|
||||
"description": "Styleframe-based design system for authentik.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/goauthentik/authentik.git",
|
||||
"directory": "packages/theme"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run build:types && npm run build:assets && npm run build:dtcg && npm run build:format",
|
||||
"build:assets": "node scripts/build.mjs",
|
||||
"build:dtcg": "styleframe dtcg export --config styleframe.config.mjs --output dist/tokens.dtcg.json --collection authentik",
|
||||
"build:format": "node scripts/format-dist.mjs",
|
||||
"build:types": "tsc -p .",
|
||||
"lint": "eslint --fix .",
|
||||
"lint-check": "eslint --max-warnings 0 .",
|
||||
"prepare": "npm run build",
|
||||
"prettier": "prettier --write .",
|
||||
"prettier-check": "prettier --check .",
|
||||
"watch": "tsc -p . --watch"
|
||||
},
|
||||
"type": "module",
|
||||
"types": "./out/index.d.ts",
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": {
|
||||
"types": "./out/index.d.ts",
|
||||
"node": "./lib/node.js",
|
||||
"browser": "./index.js",
|
||||
"import": "./index.js"
|
||||
},
|
||||
"./build": {
|
||||
"types": "./out/lib/node.d.ts",
|
||||
"import": "./lib/node.js"
|
||||
},
|
||||
"./index.css": "./dist/index.css",
|
||||
"./color.css": "./dist/color.css",
|
||||
"./typography.css": "./dist/typography.css",
|
||||
"./spacing.css": "./dist/spacing.css",
|
||||
"./shape.css": "./dist/shape.css",
|
||||
"./shadow.css": "./dist/shadow.css",
|
||||
"./motion.css": "./dist/motion.css",
|
||||
"./z-index.css": "./dist/z-index.css",
|
||||
"./tokens.dtcg.json": "./dist/tokens.dtcg.json",
|
||||
"./tokens.dtcg.resolver.json": "./dist/tokens.dtcg.resolver.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@styleframe/core": "^3.5.0",
|
||||
"@styleframe/dtcg": "^1.1.0",
|
||||
"@styleframe/theme": "^3.7.0",
|
||||
"@styleframe/transpiler": "^3.3.0",
|
||||
"styleframe": "^3.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.3",
|
||||
"@goauthentik/prettier-config": "../prettier-config",
|
||||
"@goauthentik/eslint-config": "../eslint-config",
|
||||
"@goauthentik/tsconfig": "../tsconfig",
|
||||
"@styleframe/cli": "^4.0.0",
|
||||
"@types/node": "^25.7.0",
|
||||
"eslint": "^9.39.3",
|
||||
"pino": "^10.3.1",
|
||||
"pino-pretty": "^13.1.2",
|
||||
"prettier": "^3.8.3",
|
||||
"prettier-plugin-packagejson": "^3.0.2",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.59.3"
|
||||
},
|
||||
"files": [
|
||||
"./index.js",
|
||||
"lib/**/*",
|
||||
"out/**/*",
|
||||
"dist/**/*"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=24",
|
||||
"npm": ">=11.14.1"
|
||||
},
|
||||
"devEngines": {
|
||||
"runtime": {
|
||||
"name": "node",
|
||||
"version": ">=24",
|
||||
"onFail": "ignore"
|
||||
},
|
||||
"packageManager": {
|
||||
"name": "npm",
|
||||
"version": ">=11.14.1",
|
||||
"onFail": "ignore"
|
||||
}
|
||||
},
|
||||
"prettier": "@goauthentik/prettier-config",
|
||||
"overrides": {
|
||||
"@typescript-eslint/eslint-plugin": {
|
||||
"typescript": "$typescript"
|
||||
},
|
||||
"@typescript-eslint/parser": {
|
||||
"typescript": "$typescript"
|
||||
},
|
||||
"format-imports": {
|
||||
"eslint": "$eslint"
|
||||
},
|
||||
"typescript-eslint": {
|
||||
"typescript": "$typescript"
|
||||
}
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"pino": {
|
||||
"optional": true
|
||||
},
|
||||
"pino-pretty": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* @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(", ")})`);
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* @file Post-build prettier pass over `dist/`.
|
||||
*
|
||||
* Both the styleframe transpiler and the styleframe DTCG CLI emit valid but
|
||||
* not-particularly-pretty output (tab indentation, long lines that aren't
|
||||
* wrapped, mixed spacing in shadow values). Running prettier afterwards
|
||||
* gives anyone inspecting the published artefacts — `dist/index.css`,
|
||||
* `dist/color.css`, `dist/tokens.dtcg.json`, etc. — a consistent and
|
||||
* readable shape.
|
||||
*
|
||||
* Run as the last step of `npm run build` so every file consumers see is
|
||||
* formatted with the repository's own prettier config.
|
||||
*/
|
||||
|
||||
import { readdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { extname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { AuthentikPrettierConfig } from "@goauthentik/prettier-config";
|
||||
|
||||
import prettier from "prettier";
|
||||
|
||||
const __dirname = resolve(fileURLToPath(import.meta.url), "..");
|
||||
const PACKAGE_ROOT = resolve(__dirname, "..");
|
||||
const DIST_DIR = resolve(PACKAGE_ROOT, "dist");
|
||||
|
||||
/** Parsers prettier should pick for each extension. */
|
||||
const PARSER_BY_EXT = /** @type {const} */ ({
|
||||
".css": "css",
|
||||
".json": "json",
|
||||
});
|
||||
|
||||
const entries = await readdir(DIST_DIR, { withFileTypes: true });
|
||||
let formatted = 0;
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile()) continue;
|
||||
const ext = extname(entry.name);
|
||||
const parser = PARSER_BY_EXT[/** @type {keyof typeof PARSER_BY_EXT} */ (ext)];
|
||||
if (!parser) continue;
|
||||
|
||||
const path = resolve(DIST_DIR, entry.name);
|
||||
const raw = await readFile(path, "utf-8");
|
||||
const output = await prettier.format(raw, {
|
||||
...AuthentikPrettierConfig,
|
||||
filepath: path,
|
||||
parser,
|
||||
});
|
||||
|
||||
if (output !== raw) {
|
||||
await writeFile(path, output, "utf-8");
|
||||
}
|
||||
formatted++;
|
||||
}
|
||||
|
||||
console.log(`✅ Formatted ${formatted} file(s) in dist/`);
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @file Styleframe CLI entry point.
|
||||
*
|
||||
* The styleframe CLI (used by `npm run build:dtcg`) loads this file via jiti
|
||||
* and expects a default export that is a configured {@link Styleframe} instance
|
||||
* with all variables/themes already registered.
|
||||
*
|
||||
* Importing `./lib/tokens/index.js` triggers the side-effects that register
|
||||
* every token against the shared instance; we re-export `instance` as the
|
||||
* default export so the CLI's `dtcg export` and `build` commands operate on
|
||||
* the same tree the rest of the package uses.
|
||||
*/
|
||||
|
||||
import "./lib/tokens/index.js";
|
||||
|
||||
import { instance } from "./lib/shared.js";
|
||||
|
||||
export default instance;
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "@goauthentik/tsconfig",
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext"],
|
||||
"resolveJsonModule": true,
|
||||
"checkJs": true,
|
||||
"emitDeclarationOnly": true
|
||||
},
|
||||
"exclude": [
|
||||
// ---
|
||||
"**/out/**/*"
|
||||
]
|
||||
}
|
||||
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user