Files
WRNexusJS/packages/design-system/src/presets.ts
T

256 lines
9.0 KiB
TypeScript

import type { ComponentRecipe, DesignSystem, DesignToken, RecipeParts, TokenMap } from "./types.ts";
import { catalogRecipes } from "./catalog-recipes.ts";
const ref = (name: string) => ({ ref: name });
const token = (
type: DesignToken["type"],
value: DesignToken["value"],
legacy?: string,
): DesignToken => ({ type, value, ...(legacy ? { legacy } : {}) });
function colors(values: Record<string, string>): TokenMap {
const expanded = {
...values,
"surface-raised": values.surface!,
"surface-muted": values["surface-2"]!,
"surface-soft": values["surface-2"]!,
"text-subtle": values["text-muted"]!,
"border-strong": values.border!,
"primary-muted": values["primary-soft"]!,
};
return Object.fromEntries(
Object.entries(expanded).map(([key, value]) => [
`color.${key}`,
token("color", value, `color-${key}`),
]),
);
}
function recipes(angular: boolean, outlined: boolean): Record<string, ComponentRecipe> {
const recipe = (base: RecipeParts): ComponentRecipe => ({ contractVersion: 1, base });
const surface = {
borderRadius: ref("shape.panel"),
borderWidth: "1px",
borderStyle: "solid",
borderColor: ref("color.border"),
boxShadow: ref("elevation.panel"),
};
const field = {
borderRadius: ref("shape.control"),
borderWidth: "1px",
borderStyle: "solid",
borderColor: ref("color.border"),
backgroundColor: ref("color.surface"),
color: ref("color.text"),
fontFamily: ref("type.body"),
};
const result: Record<string, ComponentRecipe> = {
Button: {
contractVersion: 1,
base: {
control: {
borderRadius: ref("shape.control"),
borderWidth: "1px",
borderStyle: "solid",
paddingInline: ref("space.control"),
boxShadow: ref("elevation.panel"),
fontFamily: ref("type.body"),
...(angular ? { rotate: "-2deg" } : {}),
},
},
variants: [
{
when: { variant: "default" },
parts: {
control: {
backgroundImage: "none",
backgroundColor: outlined ? "transparent" : ref("color.primary"),
color: outlined ? ref("color.primary") : ref("color.primary-contrast"),
borderColor: ref("color.primary"),
},
},
},
],
states: [
{ state: "focusVisible", parts: { control: { outlineColor: ref("color.primary") } } },
{
state: "disabled",
parts: { control: { opacity: 0.55, ...(angular ? { rotate: "0deg" } : {}) } },
},
],
},
Input: recipe({ control: field }),
Textarea: recipe({ control: field }),
Select: recipe({ control: field }),
Checkbox: recipe({ control: { borderRadius: ref("shape.control"), borderWidth: "1px" } }),
Radio: recipe({ control: { borderWidth: "1px" } }),
Switch: recipe({ control: { borderRadius: ref("shape.control") } }),
Card: recipe({
panel: { ...surface, ...(angular ? { rotate: "1deg" } : {}) },
body: { paddingInline: ref("space.panel"), paddingBlock: ref("space.panel") },
heading: { fontFamily: ref("type.heading") },
}),
Alert: recipe({
root: { borderRadius: ref("shape.panel"), boxShadow: ref("elevation.panel") },
}),
Modal: recipe({
panel: { ...surface, borderRadius: ref("shape.dialog") },
heading: { fontFamily: ref("type.heading") },
}),
ContextMenu: recipe({
panel: { ...surface, borderRadius: ref("shape.dialog") },
item: { borderRadius: ref("shape.control"), fontFamily: ref("type.body") },
}),
Nav: {
contractVersion: 1,
base: { link: { borderRadius: ref("shape.control"), fontFamily: ref("type.body") } },
states: [{ state: "selected", parts: { link: { fontWeight: 700 } } }],
},
PageHeader: recipe({ title: { fontFamily: ref("type.heading"), letterSpacing: "-0.025em" } }),
Container: recipe({ root: { paddingInline: ref("space.page") } }),
};
return { ...catalogRecipes(), ...result };
}
function preset(id: string, name: string, soft: boolean, angular: boolean): DesignSystem {
const mono = '"IBM Plex Mono", "Courier New", monospace';
const body = soft ? "Georgia, serif" : angular ? "Arial, sans-serif" : mono;
const heading = angular ? '"Times New Roman", serif' : body;
const light = soft
? {
bg: "#E8DCC7",
background: "#E8DCC7",
surface: "#D4B895",
"surface-2": "#E8DCC7",
text: "#292D24",
foreground: "#292D24",
"text-muted": "#4B5141",
border: "#606C38",
primary: "#606C38",
"primary-hover": "#4B542C",
"primary-active": "#3D4424",
"primary-contrast": "#FFFFFF",
"primary-text": "#3D4424",
"primary-soft": "#D4B895",
}
: angular
? {
bg: "#E8E0C0",
background: "#E8E0C0",
surface: "#EDE4CF",
"surface-2": "#E8E0C0",
text: "#232320",
foreground: "#232320",
"text-muted": "#535146",
border: "#535146",
primary: "#8F2448",
"primary-hover": "#701C39",
"primary-active": "#59172D",
"primary-contrast": "#FFFFFF",
"primary-text": "#701C39",
"primary-soft": "#E8E0C0",
}
: {
bg: "#FFFFFF",
background: "#FFFFFF",
surface: "#F7F7F8",
"surface-2": "#FFFFFF",
text: "#0B0C0A",
foreground: "#0B0C0A",
"text-muted": "#454741",
border: "#696C63",
primary: "#705000",
"primary-hover": "#594000",
"primary-active": "#443100",
"primary-contrast": "#FFFFFF",
"primary-text": "#594000",
"primary-soft": "#F7F7F8",
};
const dark = soft
? {
bg: "#292D24",
background: "#292D24",
surface: "#363D2F",
"surface-2": "#414B37",
text: "#E8DCC7",
foreground: "#E8DCC7",
"text-muted": "#C9C0AA",
border: "#8B9D83",
primary: "#D4B895",
"primary-hover": "#E8DCC7",
"primary-active": "#E8DCC7",
"primary-contrast": "#292D24",
"primary-text": "#E8DCC7",
"primary-soft": "#414B37",
}
: angular
? {
bg: "#25221E",
background: "#25221E",
surface: "#353128",
"surface-2": "#454032",
text: "#EDE4CF",
foreground: "#EDE4CF",
"text-muted": "#D0C7B2",
border: "#B0A78D",
primary: "#FF9FC6",
"primary-hover": "#FFC0DA",
"primary-active": "#FFD2E5",
"primary-contrast": "#25221E",
"primary-text": "#FF9FC6",
"primary-soft": "#454032",
}
: {
bg: "#0B0C0A",
background: "#0B0C0A",
surface: "#000000",
"surface-2": "#0B0C0A",
text: "#F7F7F8",
foreground: "#F7F7F8",
"text-muted": "#BEC0B9",
border: "#696C63",
primary: "#FFB800",
"primary-hover": "#FFD15C",
"primary-active": "#FFE099",
"primary-contrast": "#000000",
"primary-text": "#FFB800",
"primary-soft": "#0B0C0A",
};
const tokens: TokenMap = {
"shape.control": token("dimension", soft ? "16px" : "0", "radius-sm"),
"shape.panel": token("dimension", soft ? "24px" : "0", "radius-md"),
"shape.dialog": token("dimension", soft ? "32px" : "0", "radius-lg"),
"shape.base": token("dimension", soft ? "24px" : "0", "radius"),
"space.control": token("dimension", soft ? "1.25rem" : "0.75rem"),
"space.panel": token("dimension", soft ? "1.5rem" : "1rem", "space-panel"),
"space.page": token("dimension", soft ? "1.5rem" : "1rem"),
"control.height": token("dimension", soft ? "2.75rem" : "2.5rem", "control-height"),
"type.body": token("fontFamily", body, "font-sans"),
"type.heading": token("fontFamily", heading),
"elevation.panel": token("shadow", angular ? "3px 3px 0 #535146" : "none", "shadow-1"),
...colors(dark),
};
return {
schemaVersion: 1,
id,
name,
version: "1.0.0",
defaultMode: soft || angular ? "light" : "dark",
tokens,
modes: {
light: { ...colors(light), scheme: token("colorScheme", "light", "color-scheme") },
dark: { ...colors(dark), scheme: token("colorScheme", "dark", "color-scheme") },
},
recipes: recipes(angular, !soft),
};
}
/** Industrial: black/amber, monospace, square outlined controls and flat panels. */
export const SQUARE_DESIGN_SYSTEM = preset("square", "Square / Industrial", false, false);
/** Organic: sand/sage surfaces, warm serif typography, generous rounded controls. */
export const SOFT_DESIGN_SYSTEM = preset("soft", "Soft / Organic", true, false);
/** Lo-Fi: paper-yellow, mixed system typography, rotated actions and hard shadows. */
export const ANGULAR_DESIGN_SYSTEM = preset("angular", "Angular / Lo-Fi", false, true);
export const CORE_DESIGN_SYSTEMS = {
square: SQUARE_DESIGN_SYSTEM,
soft: SOFT_DESIGN_SYSTEM,
angular: ANGULAR_DESIGN_SYSTEM,
} as const;