UNPKG

@vladsolomon/tdot

Version:

Minimal runtime-configurable typography system for React + Tailwind

118 lines (111 loc) 3.64 kB
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); } import React from "react"; import { useTdotConfig } from "./provider"; import { twMerge } from "tailwind-merge"; // Allowed typography HTML tags const TYPOGRAPHY_TAGS = new Set(["h1", "h2", "h3", "h4", "h5", "h6", // Headings "p", // Paragraph "span", // Inline text "strong", "b", // Bold text "em", "i", // Italic text "u", // Underlined text "small", // Small text "mark", // Highlighted text "del", "s", // Strikethrough text "ins", // Inserted text "sub", "sup", // Subscript/Superscript "code", "kbd", "samp", "var", // Code-related "blockquote", "cite", "q", // Quotations "abbr", "dfn", // Definitions/Abbreviations "time", // Time element "address", // Address element "pre"]); // Cache for resolved component configs to avoid infinite loops const resolvedConfigCache = new Map(); function resolveComponentConfig(componentName, config, visited = new Set()) { // Check cache first if (resolvedConfigCache.has(componentName)) { return resolvedConfigCache.get(componentName); } // Prevent infinite loops if (visited.has(componentName)) { console.warn(`[Tdot] Circular dependency detected for "${componentName}"`); return null; } const entry = config[componentName]; if (!entry) { return null; } visited.add(componentName); let resolvedEntry = { ...entry }; // If extends is specified, resolve the parent first if (entry.extends) { const parentConfig = resolveComponentConfig(entry.extends, config, visited); if (parentConfig) { // Merge parent classes with current classes const parentClasses = parentConfig.classes || ""; const currentClasses = entry.classes || ""; resolvedEntry = { ...parentConfig, ...entry, classes: twMerge(parentClasses, currentClasses) }; } else { console.warn(`[Tdot] Cannot extend "${entry.extends}" for "${componentName}": parent not found`); } } visited.delete(componentName); // Cache the resolved config resolvedConfigCache.set(componentName, resolvedEntry); return resolvedEntry; } export function createTypographyComponent(componentName) { return function TypographyComponent({ children, className = "", ...rest }) { const config = useTdotConfig(); // Clear cache when config changes (simple cache invalidation) const configKeys = Object.keys(config).join(","); if (!createTypographyComponent._lastConfigKeys || createTypographyComponent._lastConfigKeys !== configKeys) { resolvedConfigCache.clear(); createTypographyComponent._lastConfigKeys = configKeys; } const entry = resolveComponentConfig(componentName, config); if (!entry || !entry.tag) { console.warn(`[Tdot] Skipping "${componentName}": missing required "tag" in config.`); return null; } const tag = entry.tag.toLowerCase(); // Validate that the tag is a typography element if (!TYPOGRAPHY_TAGS.has(tag)) { console.warn(`[Tdot] Invalid tag "${entry.tag}" for "${componentName}". Only typography elements are allowed: ${Array.from(TYPOGRAPHY_TAGS).join(", ")}`); return null; } const Tag = tag; const classes = entry.classes || ""; const mergedClassName = twMerge(classes, className); return /*#__PURE__*/React.createElement(Tag, _extends({ className: mergedClassName }, rest), children); }; }