improved a lot of things

This commit is contained in:
SinachPat
2026-04-26 09:09:55 +01:00
parent 2911f22df8
commit 97315846be
70 changed files with 4967 additions and 10 deletions
+21
View File
@@ -0,0 +1,21 @@
{
"name": "@originmain/design-language",
"version": "0.0.1",
"private": true,
"type": "module",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"typecheck": "tsc --noEmit",
"test": "vitest run --coverage"
},
"dependencies": {
"zod": "^3.0.0"
},
"devDependencies": {
"typescript": "^5.5.0",
"vitest": "^2.0.0",
"@vitest/coverage-v8": "^2.0.0"
}
}
+3
View File
@@ -0,0 +1,3 @@
export * from './schema.js';
export * from './validator.js';
export * from './tokens.js';
+111
View File
@@ -0,0 +1,111 @@
import { z } from 'zod';
// ── Token schemas ─────────────────────────────────────────────────────────────
export const ColorTokenSchema = z.object({
value: z.string().regex(/^#[0-9A-Fa-f]{3,8}$|^rgba?\(|^hsl/, 'Must be a valid CSS color'),
/** Fluent 2 token name this maps to, e.g. "colorBrandBackground" */
fluentToken: z.string().optional(),
description: z.string().optional(),
});
export const TypographyTokenSchema = z.object({
fontFamily: z.string().optional(),
fontSize: z.union([z.string(), z.number()]).optional(),
fontWeight: z.union([z.string(), z.number()]).optional(),
lineHeight: z.union([z.string(), z.number()]).optional(),
letterSpacing: z.union([z.string(), z.number()]).optional(),
fluentToken: z.string().optional(),
});
export const SpacingTokenSchema = z.object({
value: z.union([z.string(), z.number()]),
fluentToken: z.string().optional(),
});
export const MotionTokenSchema = z.object({
duration: z.string().optional(),
easing: z.string().optional(),
fluentToken: z.string().optional(),
});
export const TokensSchema = z.object({
colors: z.record(ColorTokenSchema).optional(),
typography: z.record(TypographyTokenSchema).optional(),
spacing: z.record(SpacingTokenSchema).optional(),
motion: z.record(MotionTokenSchema).optional(),
});
// ── Component rules ───────────────────────────────────────────────────────────
export const PropRuleSchema = z.object({
allowed: z.array(z.unknown()).optional(),
forbidden: z.array(z.unknown()).optional(),
required: z.boolean().optional(),
type: z.string().optional(),
});
export const ComponentRuleSchema = z.object({
/** Allowed prop values, keyed by prop name */
props: z.record(PropRuleSchema).optional(),
/** Fluent 2 variants that are explicitly forbidden */
forbiddenVariants: z.array(z.string()).optional(),
/** ARIA attributes that are required on this component */
requiredAria: z.array(z.string()).optional(),
notes: z.string().optional(),
});
// ── Screen rules ──────────────────────────────────────────────────────────────
export const ScreenRuleSchema = z.object({
/** Components that are allowed to appear on this screen */
allowedComponents: z.array(z.string()).optional(),
/** Components that must be present on this screen */
requiredSections: z.array(z.string()).optional(),
layout: z.string().optional(),
notes: z.string().optional(),
});
// ── Voice / tone ──────────────────────────────────────────────────────────────
export const VoiceRuleSchema = z.object({
tone: z.string().optional(),
maxSentenceLength: z.number().optional(),
avoidWords: z.array(z.string()).optional(),
preferWords: z.array(z.string()).optional(),
examples: z.array(z.string()).optional(),
});
// ── Accessibility ─────────────────────────────────────────────────────────────
export const AccessibilitySchema = z.object({
wcagLevel: z.enum(['A', 'AA', 'AAA']).optional(),
contrastRatio: z.number().optional(),
customRules: z.array(z.string()).optional(),
});
// ── Design Language File ──────────────────────────────────────────────────────
export const DesignLanguageFileBodySchema = z.object({
/** Semantic version, e.g. "1.0.0" */
version: z.string().optional(),
/** Human-readable name, e.g. "Acme Design System" */
name: z.string().optional(),
tokens: TokensSchema.optional(),
/** Per-component rules, keyed by component display name */
components: z.record(ComponentRuleSchema).optional(),
/** Per-screen rules, keyed by screen name or route pattern */
screens: z.record(ScreenRuleSchema).optional(),
voice: VoiceRuleSchema.optional(),
accessibility: AccessibilitySchema.optional(),
});
export type DesignLanguageFileBody = z.infer<typeof DesignLanguageFileBodySchema>;
export type Tokens = z.infer<typeof TokensSchema>;
export type ComponentRule = z.infer<typeof ComponentRuleSchema>;
export type ScreenRule = z.infer<typeof ScreenRuleSchema>;
+54
View File
@@ -0,0 +1,54 @@
import type { DesignLanguageFileBody } from './schema.js';
// ── Fluent 2 token mapping ────────────────────────────────────────────────────
// Maps DLF token names to Fluent 2 (Griffel) CSS custom property names.
// A token entry with a `fluentToken` field overrides the default Fluent 2 value.
export type FluentTokenMap = Record<string, string>;
/** Extract color token overrides from a DLF as a Fluent 2 token map. */
export function extractColorTokens(dlf: DesignLanguageFileBody): FluentTokenMap {
const out: FluentTokenMap = {};
const colors = dlf.tokens?.colors ?? {};
for (const [, token] of Object.entries(colors)) {
if (!token) continue;
if (token.fluentToken) {
out[token.fluentToken] = token.value;
}
}
return out;
}
/** Convert a Fluent 2 token map to CSS custom properties for injection. */
export function tokensToCssVars(tokens: FluentTokenMap): string {
const entries = Object.entries(tokens)
.map(([name, value]) => ` --${camelToKebab(name)}: ${value};`)
.join('\n');
return `:root {\n${entries}\n}`;
}
/** Convert a DLF to a CSS var block suitable for injection into the renderer iframe. */
export function dlfToCssVars(dlf: DesignLanguageFileBody): string {
return tokensToCssVars(extractColorTokens(dlf));
}
// ── Helpers ───────────────────────────────────────────────────────────────────
function camelToKebab(str: string): string {
return str.replace(/([A-Z])/g, m => `-${m.toLowerCase()}`);
}
/** Build a short human-readable summary of the DLF's token count for logging. */
export function dlfSummary(dlf: DesignLanguageFileBody): string {
const tokenCounts = {
colors: Object.keys(dlf.tokens?.colors ?? {}).length,
typography: Object.keys(dlf.tokens?.typography ?? {}).length,
spacing: Object.keys(dlf.tokens?.spacing ?? {}).length,
components: Object.keys(dlf.components ?? {}).length,
screens: Object.keys(dlf.screens ?? {}).length,
};
return Object.entries(tokenCounts)
.filter(([, v]) => v > 0)
.map(([k, v]) => `${v} ${k}`)
.join(', ');
}
+89
View File
@@ -0,0 +1,89 @@
import { DesignLanguageFileBodySchema, type DesignLanguageFileBody } from './schema.js';
import type { ZodError } from 'zod';
// ── Validation result ─────────────────────────────────────────────────────────
export interface ValidationSuccess {
valid: true;
dlf: DesignLanguageFileBody;
}
export interface ValidationFailure {
valid: false;
errors: ValidationError[];
}
export interface ValidationError {
path: string;
message: string;
}
export type ValidationResult = ValidationSuccess | ValidationFailure;
// ── Parse & validate ──────────────────────────────────────────────────────────
export function validateDesignLanguageFile(input: unknown): ValidationResult {
const result = DesignLanguageFileBodySchema.safeParse(input);
if (result.success) {
return { valid: true, dlf: result.data };
}
return { valid: false, errors: formatZodErrors(result.error) };
}
function formatZodErrors(error: ZodError): ValidationError[] {
return error.errors.map(e => ({
path: e.path.join('.') || '(root)',
message: e.message,
}));
}
// ── Runtime constraint checks ─────────────────────────────────────────────────
// These run during visual edits and AI completions to catch violations
// against the active DLF before they're shown to the user.
export interface ViolationCheck {
/** Name of the component being checked */
componentName: string;
/** Props being applied */
props: Record<string, unknown>;
/** The active DLF */
dlf: DesignLanguageFileBody;
}
export interface Violation {
prop: string;
value: unknown;
message: string;
severity: 'error' | 'warning';
}
export function checkComponentConstraints(check: ViolationCheck): Violation[] {
const { componentName, props, dlf } = check;
const violations: Violation[] = [];
const componentRule = dlf.components?.[componentName];
if (!componentRule) return violations;
const { props: propRules } = componentRule;
if (!propRules) return violations;
for (const [propKey, rule] of Object.entries(propRules)) {
const value = props[propKey];
// Use hasOwnProperty to distinguish "key absent" from "key set to undefined".
// Under exactOptionalPropertyTypes these are semantically different.
if (rule.required && !Object.prototype.hasOwnProperty.call(props, propKey)) {
violations.push({ prop: propKey, value, message: `"${propKey}" is required by design system rules`, severity: 'error' });
}
if (rule.forbidden && value !== undefined && rule.forbidden.includes(value)) {
violations.push({ prop: propKey, value, message: `"${propKey}=${String(value)}" is forbidden by design system rules`, severity: 'error' });
}
if (rule.allowed && value !== undefined && !rule.allowed.includes(value)) {
violations.push({ prop: propKey, value, message: `"${propKey}=${String(value)}" is not in the allowed values list`, severity: 'warning' });
}
}
return violations;
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"noEmit": true
},
"include": ["src"],
"exclude": ["node_modules"]
}