106 lines
3.0 KiB
TypeScript
106 lines
3.0 KiB
TypeScript
import type { DesignKitOverrides, StarterKitDefinition } from "./types";
|
|
|
|
export interface ResolvedKitTokens {
|
|
accentHex: string;
|
|
accentScale: string[];
|
|
grayScale: string[];
|
|
radiusMdPx: number;
|
|
radiusXs: number;
|
|
radiusSm: number;
|
|
radiusXl: number;
|
|
radiusXxl: number;
|
|
fontFamily: string;
|
|
density: "compact" | "comfortable";
|
|
}
|
|
|
|
/** Fixed CRM-neutral ramp (custom neutral tint could merge later). */
|
|
export const GRAY_TWELVE: string[] = [
|
|
"#ffffff",
|
|
"#fafafa",
|
|
"#f5f5f5",
|
|
"#ebebeb",
|
|
"#e0e0e0",
|
|
"#d4d4d4",
|
|
"#c4c4c4",
|
|
"#a3a3a3",
|
|
"#737373",
|
|
"#525252",
|
|
"#3f3f3f",
|
|
"#1a1a1a",
|
|
];
|
|
|
|
function parseHex(hex: string): [number, number, number] | null {
|
|
const n = hex.trim().replace(/^#/, "");
|
|
if (n.length !== 6 || !/^[0-9a-fA-F]+$/.test(n)) return null;
|
|
return [parseInt(n.slice(0, 2), 16), parseInt(n.slice(2, 4), 16), parseInt(n.slice(4, 6), 16)];
|
|
}
|
|
|
|
function rgbToHex(r: number, g: number, b: number): string {
|
|
const x = (v: number) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0");
|
|
return `#${x(r)}${x(g)}${x(b)}`;
|
|
}
|
|
|
|
/** Linear RGB mix (good enough for UI ramps). */
|
|
export function mixHex(a: string, b: string, t: number): string {
|
|
const A = parseHex(a);
|
|
const B = parseHex(b);
|
|
if (!A || !B) return a;
|
|
const r = Math.round(A[0] + (B[0] - A[0]) * t);
|
|
const g = Math.round(A[1] + (B[1] - A[1]) * t);
|
|
const bl = Math.round(A[2] + (B[2] - A[2]) * t);
|
|
return rgbToHex(r, g, bl);
|
|
}
|
|
|
|
/** Twelve-step accent ramp: light tints → base → deep shadows. */
|
|
export function buildAccentScale(base: string): string[] {
|
|
const steps: string[] = [];
|
|
for (let i = 0; i < 12; i++) {
|
|
const t = i / 11;
|
|
if (t <= 0.45) {
|
|
steps.push(mixHex("#ffffff", base, t / 0.45 * 0.92));
|
|
} else {
|
|
steps.push(mixHex(base, "#0c0c12", (t - 0.45) / 0.55 * 0.92));
|
|
}
|
|
}
|
|
return steps;
|
|
}
|
|
|
|
export function mergeOverrides(
|
|
kit: StarterKitDefinition,
|
|
saved?: DesignKitOverrides,
|
|
): Required<
|
|
Pick<DesignKitOverrides, "accentHex" | "radiusMdPx" | "fontPreset" | "density">
|
|
> {
|
|
const d = kit.defaults;
|
|
const o = saved ?? {};
|
|
return {
|
|
accentHex: o.accentHex ?? d.accentHex ?? "#5f6bf5",
|
|
radiusMdPx: o.radiusMdPx ?? d.radiusMdPx ?? 8,
|
|
fontPreset: o.fontPreset ?? d.fontPreset ?? "inter",
|
|
density: o.density ?? d.density ?? "comfortable",
|
|
};
|
|
}
|
|
|
|
export function resolveKitTokens(
|
|
kit: StarterKitDefinition,
|
|
saved?: DesignKitOverrides,
|
|
): ResolvedKitTokens {
|
|
const m = mergeOverrides(kit, saved);
|
|
const md = m.radiusMdPx;
|
|
return {
|
|
accentHex: m.accentHex,
|
|
accentScale: buildAccentScale(m.accentHex),
|
|
grayScale: GRAY_TWELVE,
|
|
radiusMdPx: md,
|
|
radiusXs: Math.max(2, Math.round(md * 0.25)),
|
|
radiusSm: Math.max(4, Math.round(md * 0.5)),
|
|
radiusXl: Math.max(md + 4, Math.round(md * 2.25)),
|
|
radiusXxl: Math.max(md + 8, Math.round(md * 4.5)),
|
|
fontFamily:
|
|
m.fontPreset === "system"
|
|
? 'system-ui, -apple-system, "Segoe UI", sans-serif'
|
|
: "var(--font-inter), ui-sans-serif, system-ui, sans-serif",
|
|
density: m.density,
|
|
};
|
|
}
|