= ({ group }) => {
+ const { colorMode } = useColorMode();
+
+ if (!group) {
+ throw new TypeError("Invalid color group name");
+ }
+
+ const computed = useMemo(() => createComputedColorGroup(group, colorMode), [group, colorMode]);
+
+ return (
+
+ {computed.colors.map((color) => (
+
+ ))}
+
+ );
+};
+
+export interface PaletteGroupProps {
+ entries?: Iterable;
+}
+
+export const PaletteGroup: React.FC = ({ entries }) => {
+ const { colorMode } = useColorMode();
+
+ if (!entries) {
+ throw new TypeError("Invalid utility color entries");
+ }
+
+ const swatchProps: ColorSwatchProps[] = useMemo(() => {
+ return Array.from(entries, ([label, partialCSSVar]) => {
+ const cssVar = `${Prefix.Infima}${partialCSSVar}`;
+ const { hex, contrastColor } = computeColor(cssVar);
+
+ return { cssVar, label, hex, contrastColor, showVar: true, colorMode };
+ });
+ }, [entries, colorMode]);
+
+ return (
+
+ {swatchProps.map((props) => (
+
+ ))}
+
+ );
+};
diff --git a/website/docusaurus-theme/components/infima/Swatch/styles.module.css b/website/docusaurus-theme/components/infima/Swatch/styles.module.css
new file mode 100644
index 0000000000..ece6d540bd
--- /dev/null
+++ b/website/docusaurus-theme/components/infima/Swatch/styles.module.css
@@ -0,0 +1,61 @@
+.colorGrid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+ gap: var(--ifm-global-spacing);
+}
+
+.utilityGrid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
+ gap: var(--ifm-global-spacing);
+}
+
+.swatch {
+ padding: 12px 16px;
+ border-radius: var(--ifm-global-radius);
+ cursor: pointer;
+ transition: all 0.2s ease;
+ border: 1px solid rgba(128, 128, 128, 0.15);
+ position: relative;
+ min-height: 70px;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ box-shadow: var(--ifm-global-shadow-lw);
+}
+
+.swatch:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+.swatchLabel {
+ font-weight: 600;
+ font-size: 13px;
+ margin-bottom: 4px;
+}
+
+.swatchVar {
+ opacity: 0.8;
+ word-break: break-all;
+ font-family: var(--ifm-font-family-monospace);
+ font-weight: var(--ifm-font-weight-semibold);
+ font-size: 0.75rem;
+}
+
+.swatchHex {
+ font-size: 0.875rem;
+ font-family: var(--ifm-font-family-monospace);
+}
+
+.copiedToast {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background: rgba(0, 0, 0, 0.85);
+ color: #fff;
+ padding: 6px 12px;
+ border-radius: var(--ifm-global-radius);
+ font-weight: 500;
+}
diff --git a/website/docusaurus-theme/components/infima/constants.ts b/website/docusaurus-theme/components/infima/constants.ts
new file mode 100644
index 0000000000..427730a4c6
--- /dev/null
+++ b/website/docusaurus-theme/components/infima/constants.ts
@@ -0,0 +1,126 @@
+import {
+ ColorEntry,
+ ColorGroupProp,
+ Shade,
+} from "@goauthentik/docusaurus-theme/components/infima/shared.ts";
+
+const shades: Shade[] = [
+ ["Darkest", "-darkest"],
+ ["Darker", "-darker"],
+ ["Dark", "-dark"],
+ ["Default", ""],
+ ["Light", "-light"],
+ ["Lighter", "-lighter"],
+ ["Lightest", "-lightest"],
+];
+
+export const InfimaColorsMap: ReadonlyMap = new Map([
+ [
+ "primary",
+ {
+ label: "Primary",
+ cssVar: "color-primary",
+ shades,
+ },
+ ],
+ [
+ "secondary",
+ {
+ label: "Secondary",
+ cssVar: "color-secondary",
+ shades,
+ },
+ ],
+ [
+ "success",
+ {
+ label: "Success",
+ cssVar: "color-success",
+ shades,
+ },
+ ],
+ [
+ "info",
+ {
+ label: "Info",
+ cssVar: "color-info",
+ shades,
+ },
+ ],
+ [
+ "warning",
+ {
+ label: "Warning",
+ cssVar: "color-warning",
+ shades,
+ },
+ ],
+ [
+ "danger",
+ {
+ label: "Danger",
+ cssVar: "color-danger",
+ shades,
+ },
+ ],
+]);
+
+export const UtilityColorEntries: readonly ColorEntry[] = [
+ ["Background Color", "background-color"],
+ ["Background Surface", "background-surface-color"],
+ ["Font Color Base", "font-color-base"],
+ ["Font Color Secondary", "font-color-secondary"],
+ ["Content Color", "color-content"],
+ ["Content Color Inverse", "color-content-inverse"],
+ ["Content Color Secondary", "color-content-secondary"],
+ ["Heading Color", "heading-color"],
+ ["Link Color", "link-color"],
+ ["Menu Color", "menu-color"],
+ ["Menu Color Active", "menu-color-active"],
+ ["Navbar Background", "navbar-background-color"],
+ ["Footer Background", "footer-background-color"],
+ ["Card Background", "card-background-color"],
+ ["Code Background", "code-background"],
+ ["Toc Border", "toc-border-color"],
+ ["Table Stripe", "table-stripe-background"],
+ ["Hover Overlay", "hover-overlay"],
+];
+
+export const DispositionInfoColorEntries: readonly ColorEntry[] = [
+ ["Contrast Background", "color-info-contrast-background"],
+ ["Dark", "color-info-dark"],
+ ["Darker", "color-info-darker"],
+ ["Darkest", "color-info-darkest"],
+ ["Light", "color-info-light"],
+ ["Lighter", "color-info-lighter"],
+ ["Lightest", "color-info-lightest"],
+];
+
+export const DispositionSuccessColorEntries: readonly ColorEntry[] = [
+ ["Contrast Background", "color-success-contrast-background"],
+ ["Dark", "color-success-dark"],
+ ["Darker", "color-success-darker"],
+ ["Darkest", "color-success-darkest"],
+ ["Light", "color-success-light"],
+ ["Lighter", "color-success-lighter"],
+ ["Lightest", "color-success-lightest"],
+];
+export const DispositionWarningColorEntries: readonly ColorEntry[] = [
+ ["Contrast Background", "color-warning-contrast-background"],
+ ["Dark", "color-warning-dark"],
+ ["Darker", "color-warning-darker"],
+ ["Darkest", "color-warning-darkest"],
+ ["Light", "color-warning-light"],
+ ["Lighter", "color-warning-lighter"],
+ ["Lightest", "color-warning-lightest"],
+];
+
+export const DispositionDangerColorEntries: readonly ColorEntry[] = [
+ ["Contrast Background", "color-danger-contrast-background"],
+ ["Dark", "color-danger-dark"],
+ ["Darker", "color-danger-darker"],
+ ["Darkest", "color-danger-darkest"],
+ ["Light", "color-danger-light"],
+ ["Lighter", "color-danger-lighter"],
+ ["Lightest", "color-danger-lightest"],
+];
diff --git a/website/docusaurus-theme/components/infima/shared.ts b/website/docusaurus-theme/components/infima/shared.ts
new file mode 100644
index 0000000000..7b6667723f
--- /dev/null
+++ b/website/docusaurus-theme/components/infima/shared.ts
@@ -0,0 +1,82 @@
+import { ColorMode } from "@docusaurus/theme-common";
+
+export type Shade = [label: string, suffix: string];
+
+export interface ColorGroupProp {
+ label: string;
+ cssVar: string;
+ shades: Shade[];
+}
+
+export type ColorEntry = [label: string, cssVar: string];
+
+export const Prefix = {
+ Infima: "--ifm-",
+ Authentik: "--ak-",
+} as const satisfies Record;
+
+export interface ComputedColor {
+ cssVar: string;
+ label: string;
+ hex: string | null;
+ contrastColor: string;
+}
+
+export function getContrastColor(hexColor: string | null): string {
+ if (!hexColor || hexColor === "transparent" || hexColor.startsWith("rgba")) {
+ return "#000000";
+ }
+ const hex = hexColor.replace("#", "");
+ if (hex.length !== 6) return "#000000";
+ const r = parseInt(hex.substring(0, 2), 16);
+ const g = parseInt(hex.substring(2, 4), 16);
+ const b = parseInt(hex.substring(4, 6), 16);
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
+ return luminance > 0.5 ? "#1a1a1a" : "#ffffff";
+}
+
+export function rgbToHex(rgb: string): string | null {
+ if (!rgb || rgb === "transparent") return null;
+ if (rgb.startsWith("#")) return rgb;
+ const match = rgb.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)/);
+ if (!match) return null;
+
+ const [, r = "", g = "", b = ""] = match;
+
+ const hex = (x: string): string => parseInt(x, 10).toString(16).padStart(2, "0");
+ return `#${hex(r)}${hex(g)}${hex(b)}`;
+}
+
+export function computeColor(cssVar: string): Pick {
+ if (typeof document === "undefined") {
+ return { hex: null, contrastColor: "#000000" };
+ }
+ const computedColor = getComputedStyle(document.documentElement)
+ .getPropertyValue(cssVar)
+ .trim();
+
+ const hex = rgbToHex(computedColor) || computedColor || null;
+ const contrastColor = getContrastColor(hex);
+
+ return { hex, contrastColor };
+}
+
+export interface ComputedColorGroup {
+ label: string;
+ colors: ComputedColor[];
+}
+
+export function createComputedColorGroup(
+ group: ColorGroupProp,
+ _colorMode: ColorMode,
+): ComputedColorGroup {
+ return {
+ label: group.label,
+ colors: group.shades.map(([label, suffix]) => {
+ const cssVar = `${Prefix.Infima}${group.cssVar}${suffix}`;
+ const { hex, contrastColor } = computeColor(cssVar);
+
+ return { cssVar, label, hex, contrastColor };
+ }),
+ };
+}