///
import {
type Cell,
cell,
Default,
handler,
lift,
recipe,
str,
} from "commontools";
interface TokenDefinition {
background: string;
foreground: string;
accent: string;
}
type TokenCatalog = Record;
const DEFAULT_TOKEN_CATALOG: TokenCatalog = {
light: {
background: "#ffffff",
foreground: "#161616",
accent: "#2f80ed",
},
midnight: {
background: "#0b1220",
foreground: "#f5f7fb",
accent: "#5b8def",
},
contrast: {
background: "#000000",
foreground: "#ffdd00",
accent: "#ff6f61",
},
};
interface DesignTokenSwitcherArgs {
tokens: Default;
activeToken: Default;
}
const sanitizeTokenName = (value: unknown): string | undefined => {
if (typeof value !== "string") return undefined;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
};
const sanitizeColor = (value: unknown, fallback: string): string => {
if (typeof value === "string") {
const trimmed = value.trim();
if (trimmed.length > 0) return trimmed;
}
return fallback;
};
const sanitizeDefinition = (
value: unknown,
fallback: TokenDefinition,
): TokenDefinition => {
if (!value || typeof value !== "object") {
return { ...fallback };
}
const record = value as Record;
return {
background: sanitizeColor(record.background, fallback.background),
foreground: sanitizeColor(record.foreground, fallback.foreground),
accent: sanitizeColor(record.accent, fallback.accent),
};
};
const sanitizeCatalog = (value: unknown): TokenCatalog => {
const base: TokenCatalog = { ...DEFAULT_TOKEN_CATALOG };
if (!value || typeof value !== "object") {
return base;
}
const record = value as Record;
for (const key of Object.keys(record)) {
const definition = record[key];
const fallback = base[key] ?? DEFAULT_TOKEN_CATALOG.light;
base[key] = sanitizeDefinition(definition, fallback);
}
return base;
};
const findNextName = (current: string, names: string[]): string => {
if (names.length === 0) return current;
const index = names.indexOf(current);
if (index === -1) return names[0];
return names[(index + 1) % names.length];
};
const switchDesignToken = handler(
(
event: { token?: string } | undefined,
context: {
active: Cell;
tokens: Cell;
history: Cell;
},
) => {
const catalog = sanitizeCatalog(context.tokens.get());
const names = Object.keys(catalog).sort((a, b) => a.localeCompare(b));
const fallback = names[0] ?? "light";
const current = sanitizeTokenName(context.active.get()) ?? fallback;
const requested = sanitizeTokenName(event?.token);
const next = requested && names.includes(requested)
? requested
: findNextName(current, names);
context.active.set(next);
const history = Array.isArray(context.history.get())
? context.history.get()
: [];
context.history.set([...history, next]);
},
);
export const designTokenSwitcher = recipe(
"Design Token Switcher",
({ tokens, activeToken }) => {
const appliedHistory = cell([]);
const sanitizedCatalog = lift((value: TokenCatalog | undefined) =>
sanitizeCatalog(value)
)(tokens);
const tokenNames = lift((catalog: TokenCatalog) =>
Object.keys(catalog).sort((a, b) => a.localeCompare(b))
)(sanitizedCatalog);
const currentToken = lift((input: {
candidate?: string;
names: string[];
}) => {
const names = input.names;
if (names.length === 0) return "light";
const sanitized = sanitizeTokenName(input.candidate);
return sanitized && names.includes(sanitized) ? sanitized : names[0];
})({ candidate: activeToken, names: tokenNames });
const currentDefinition = lift((input: {
name: string;
catalog: TokenCatalog;
}) =>
input.catalog[input.name] ??
sanitizeDefinition(undefined, DEFAULT_TOKEN_CATALOG.light)
)({ name: currentToken, catalog: sanitizedCatalog });
const backgroundColor = lift((definition: TokenDefinition) =>
definition.background
)(currentDefinition);
const foregroundColor = lift((definition: TokenDefinition) =>
definition.foreground
)(currentDefinition);
const accentColor = lift((definition: TokenDefinition) =>
definition.accent
)(currentDefinition);
const colorSummary = lift((definition: TokenDefinition) =>
`${definition.background}/${definition.foreground}/${definition.accent}`
)(currentDefinition);
const preview = lift((definition: TokenDefinition) => ({
background: definition.background,
foreground: definition.foreground,
accent: definition.accent,
summary: `bg ${definition.background} fg ${definition.foreground}`,
}))(currentDefinition);
const historyView = lift((value: string[] | undefined) =>
Array.isArray(value) ? value : []
)(appliedHistory);
const lastApplied = lift((value: string[]) => {
if (value.length === 0) return "none";
return value[value.length - 1];
})(historyView);
return {
tokens: sanitizedCatalog,
tokenNames,
activeToken: currentToken,
backgroundColor,
foregroundColor,
accentColor,
preview,
history: historyView,
lastApplied,
colorSummary,
label: str`Active token ${currentToken} renders ${colorSummary}`,
switchToken: switchDesignToken({
active: activeToken,
tokens,
history: appliedHistory,
}),
};
},
);