feat(studio): expand manual typography controls and theme recipes
Quality / quality (ubuntu-latest) (push) Failing after 6m43s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
Clintchiz committed 2026-10-19 21:17:50 +05:30
1 parent e0298ac471
commit 7ef475119a
10 files changed
+392 -13

No files matched your search

+138 -1
View File
@@ -1,4 +1,9 @@
import { validateDesignSystem, type DesignSystem, type DesignToken } from "@wrnexus/design-system";
import {
COMPONENT_CONTRACTS,
validateDesignSystem,
type DesignSystem,
type DesignToken,
} from "@wrnexus/design-system";
export const MANUAL_COLORS = {
primary: "Brand / primary",
@@ -16,10 +21,122 @@ export const MANUAL_COLORS = {
} as const;
export const MANUAL_FONTS = {
sans: '"Helvetica Neue", Arial, sans-serif',
system: "system-ui, sans-serif",
arial: "Arial, Helvetica, sans-serif",
verdana: "Verdana, Geneva, sans-serif",
tahoma: "Tahoma, sans-serif",
georgia: "Georgia, serif",
times: '"Times New Roman", Times, serif',
palatino: '"Palatino Linotype", Palatino, serif',
consolas: 'Consolas, "Courier New", monospace',
humanist: '"Trebuchet MS", Arial, sans-serif',
serif: 'Georgia, "Times New Roman", serif',
mono: '"Courier New", monospace',
};
export const MANUAL_TYPOGRAPHY = {
"type.body-size": {
label: "Body size (px)",
type: "dimension",
min: 10,
max: 32,
initial: 16,
unit: "px",
},
"type.heading-size": {
label: "Heading size (px)",
type: "dimension",
min: 16,
max: 96,
initial: 28,
unit: "px",
},
"type.code-size": {
label: "Code size (px)",
type: "dimension",
min: 10,
max: 32,
initial: 14,
unit: "px",
},
"type.body-weight": {
label: "Body weight",
type: "fontWeight",
min: 100,
max: 900,
initial: 400,
unit: "",
},
"type.heading-weight": {
label: "Heading weight",
type: "fontWeight",
min: 100,
max: 900,
initial: 700,
unit: "",
},
"type.body-leading": {
label: "Body line height",
type: "number",
min: 1,
max: 2.5,
initial: 1.5,
unit: "",
},
"type.heading-leading": {
label: "Heading line height",
type: "number",
min: 1,
max: 2,
initial: 1.2,
unit: "",
},
"type.body-tracking": {
label: "Body letter spacing (px)",
type: "dimension",
min: -2,
max: 8,
initial: 0,
unit: "px",
},
"type.heading-tracking": {
label: "Heading letter spacing (px)",
type: "dimension",
min: -4,
max: 12,
initial: 0,
unit: "px",
},
} as const;
function typographyRecipes(system: DesignSystem, token: string) {
const suffix = token.replace(/^type\./, "");
const group = suffix.split("-")[0]!;
const property =
(
{
size: "fontSize",
weight: "fontWeight",
leading: "lineHeight",
tracking: "letterSpacing",
} as Record<string, string>
)[suffix.split("-")[1]!] ?? "fontFamily";
for (const [name, contract] of Object.entries(COMPONENT_CONTRACTS)) {
for (const [part, descriptor] of Object.entries(contract.parts)) {
const properties: readonly string[] = descriptor.properties;
if (!properties.includes(property)) continue;
const code = /code|kbd/i.test(name) || /code/i.test(part);
const heading = /title|heading|headline/i.test(part);
if ((code ? "code" : heading ? "heading" : "body") !== group) continue;
// Keep icon geometry and managed/provider content independent of text settings.
if (
/icon|spinner|thumb|track|srOnly/i.test(part) ||
("ownership" in descriptor && descriptor.ownership !== "framework")
)
continue;
const recipe = ((system.recipes ??= {})[name] ??= { contractVersion: 1 });
((recipe.base ??= {})[part] ??= {})[property] = { ref: token };
}
}
}
export type ManualEdit =
| { kind: "color"; mode: string; name: string; value: string }
| { kind: "token"; name: string; value: string }
@@ -157,6 +274,13 @@ export function manualDesign(source: DesignSystem, edit: ManualEdit): DesignSyst
"shape.dialog": ["dimension", "radius-lg"],
"type.body": ["fontFamily", "font-sans"],
"type.heading": ["fontFamily", undefined],
"type.code": ["fontFamily", "font-mono"],
...Object.fromEntries(
Object.entries(MANUAL_TYPOGRAPHY).map(([name, setting]) => [
name,
[setting.type, undefined],
]),
),
"elevation.panel": ["shadow", "shadow-1"],
"space.control": ["dimension", undefined],
"space.panel": ["dimension", "space-panel"],
@@ -165,11 +289,24 @@ export function manualDesign(source: DesignSystem, edit: ManualEdit): DesignSyst
const [type, legacy] = allowed[edit.name]!;
if (
type === "dimension" &&
!Object.hasOwn(MANUAL_TYPOGRAPHY, edit.name) &&
(!/^\d+(?:\.\d+)?px$/.test(edit.value) ||
parseFloat(edit.value) > (edit.name.startsWith("shape.") ? 999 : 64))
)
throw Error("Enter corners between 0 and 999 pixels, or padding between 0 and 64 pixels.");
const typography = MANUAL_TYPOGRAPHY[edit.name as keyof typeof MANUAL_TYPOGRAPHY];
if (typography) {
const numeric = Number(typography.unit ? edit.value.replace(/px$/, "") : edit.value);
if (
!Number.isFinite(numeric) ||
numeric < typography.min ||
numeric > typography.max ||
(typography.unit && !/^-?\d+(?:\.\d+)?px$/.test(edit.value))
)
throw Error(`${typography.label} must be between ${typography.min} and ${typography.max}.`);
}
set(system.tokens, edit.name, edit.value, type, legacy);
if (edit.name.startsWith("type.")) typographyRecipes(system, edit.name);
// Existing mode overrides must not silently win over a shared foundation edit.
for (const layer of Object.values(system.modes ?? {}))
if (layer[edit.name]) set(layer, edit.name, edit.value, type, legacy);