mirror of
https://github.com/goauthentik/authentik.git
synced 2026-06-17 19:09:11 +03:00
website/docs: Color Palette Test Page (#19879)
* Flesh out. * Flesh out. * Remove outdated version.
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
---
|
||||
title: Documentation Theming
|
||||
sidebar_label: Theming
|
||||
---
|
||||
|
||||
import {
|
||||
PaletteGroup,
|
||||
ColorGroup,
|
||||
} from "@goauthentik/docusaurus-theme/components/infima/Swatch/index.tsx";
|
||||
import {
|
||||
DispositionDangerColorEntries,
|
||||
DispositionInfoColorEntries,
|
||||
DispositionSuccessColorEntries,
|
||||
DispositionWarningColorEntries,
|
||||
InfimaColorsMap,
|
||||
UtilityColorEntries,
|
||||
} from "@goauthentik/docusaurus-theme/components/infima/constants.ts";
|
||||
|
||||
:::info Advanced
|
||||
|
||||
This section is intended for developers of authentik's documentation site. If you are looking to customize the theming of your own authentik instance, please refer to the [branding](../../../sys-mgmt/brands.md) documentation.
|
||||
|
||||
:::
|
||||
|
||||
The authentik documentation site is built using Meta's [Docusaurus](https://docusaurus.io/), which uses their internal [Infima CSS framework](https://infima.dev/) for its styling and theming capabilities. Infima's own documentation is limited, possibly due to it's internal nature and Docusaurus being the primary consumer. This document aims to provide an overview of how theming is handled in the authentik documentation site, and how you can customize it.
|
||||
|
||||
## Infima Color Palette
|
||||
|
||||
With the exception of a few customizations, our color palette is managed through Infima's theming system.
|
||||
|
||||
### Primary
|
||||
|
||||
<ColorGroup group={InfimaColorsMap.get("primary")} />
|
||||
|
||||
### Secondary
|
||||
|
||||
<ColorGroup group={InfimaColorsMap.get("secondary")} />
|
||||
|
||||
### Success
|
||||
|
||||
<ColorGroup group={InfimaColorsMap.get("success")} />
|
||||
|
||||
### Info
|
||||
|
||||
<ColorGroup group={InfimaColorsMap.get("info")} />
|
||||
|
||||
### Warning
|
||||
|
||||
<ColorGroup group={InfimaColorsMap.get("warning")} />
|
||||
|
||||
### Danger
|
||||
|
||||
<ColorGroup group={InfimaColorsMap.get("danger")} />
|
||||
|
||||
## Utility & UI Colors
|
||||
|
||||
<PaletteGroup entries={UtilityColorEntries} />
|
||||
|
||||
## Dispositions
|
||||
|
||||
### Info
|
||||
|
||||
<PaletteGroup entries={DispositionInfoColorEntries} />
|
||||
|
||||
### Success
|
||||
|
||||
<PaletteGroup entries={DispositionSuccessColorEntries} />
|
||||
|
||||
### Warning
|
||||
|
||||
<PaletteGroup entries={DispositionWarningColorEntries} />
|
||||
|
||||
### Danger
|
||||
|
||||
<PaletteGroup entries={DispositionDangerColorEntries} />
|
||||
@@ -857,6 +857,7 @@ const items = [
|
||||
},
|
||||
items: [
|
||||
"developer-docs/docs/style-guide",
|
||||
"developer-docs/docs/theming/index",
|
||||
{
|
||||
type: "category",
|
||||
label: "Templates",
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
import {
|
||||
ColorEntry,
|
||||
ColorGroupProp,
|
||||
computeColor,
|
||||
ComputedColor,
|
||||
createComputedColorGroup,
|
||||
Prefix,
|
||||
} from "@goauthentik/docusaurus-theme/components/infima/shared.ts";
|
||||
|
||||
import { useColorMode } from "@docusaurus/theme-common";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
interface ColorSwatchProps extends ComputedColor {
|
||||
showVar?: boolean;
|
||||
}
|
||||
|
||||
const ColorSwatch: React.FC<ColorSwatchProps> = ({
|
||||
cssVar,
|
||||
label,
|
||||
hex,
|
||||
contrastColor,
|
||||
showVar = true,
|
||||
}) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const copyToClipboard = async (): Promise<void> => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(cssVar);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy:", err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={copyToClipboard}
|
||||
className={styles.swatch}
|
||||
style={{
|
||||
backgroundColor: `var(${cssVar})`,
|
||||
color: contrastColor,
|
||||
}}
|
||||
>
|
||||
<div className={styles.swatchLabel}>{label}</div>
|
||||
{showVar ? <div className={styles.swatchVar}>{cssVar}</div> : null}
|
||||
<div className={styles.swatchHex}>{hex || "—"}</div>
|
||||
{copied ? <div className={styles.copiedToast}>Copied!</div> : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export interface ColorGroupProps {
|
||||
group?: ColorGroupProp;
|
||||
}
|
||||
|
||||
export const ColorGroup: React.FC<ColorGroupProps> = ({ group }) => {
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
if (!group) {
|
||||
throw new TypeError("Invalid color group name");
|
||||
}
|
||||
|
||||
const computed = useMemo(() => createComputedColorGroup(group, colorMode), [group, colorMode]);
|
||||
|
||||
return (
|
||||
<div className={styles.colorGrid}>
|
||||
{computed.colors.map((color) => (
|
||||
<ColorSwatch
|
||||
key={color.cssVar}
|
||||
cssVar={color.cssVar}
|
||||
label={color.label}
|
||||
hex={color.hex}
|
||||
contrastColor={color.contrastColor}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export interface PaletteGroupProps {
|
||||
entries?: Iterable<ColorEntry>;
|
||||
}
|
||||
|
||||
export const PaletteGroup: React.FC<PaletteGroupProps> = ({ 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 (
|
||||
<div className={styles.colorGrid}>
|
||||
{swatchProps.map((props) => (
|
||||
<ColorSwatch key={props.cssVar} {...props} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<string, ColorGroupProp> = 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"],
|
||||
];
|
||||
@@ -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<string, string>;
|
||||
|
||||
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<ComputedColor, "hex" | "contrastColor"> {
|
||||
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 };
|
||||
}),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user