UNPKG

arto

Version:

Arto is a flexible and type-safe class name management library for building scalable UIs with variants, states, and conditional styling.

827 lines (826 loc) 27.7 kB
var b = Object.defineProperty; var V = (a, s, t) => s in a ? b(a, s, { enumerable: !0, configurable: !0, writable: !0, value: t }) : a[s] = t; var o = (a, s, t) => V(a, typeof s != "symbol" ? s + "" : s, t); class A extends Error { /** * Constructs a new ArtoError instance with a prefixed message. * @param message - The original error message (automatically prefixed). */ constructor(s) { super(`[Arto Error]: ${s}`), this.name = "ArtoError"; } } const g = (a) => { throw new A(a); }, h = (a, s) => { const t = /* @__PURE__ */ new Set(), e = [a], i = /* @__PURE__ */ new Set(); for (; e.length > 0; ) { const n = e.pop(); if (n != null) if (typeof n == "string") n.split(/\s+/).filter(Boolean).forEach((r) => t.add(r)); else if (Array.isArray(n)) for (let r = n.length - 1; r >= 0; r--) e.push(n[r]); else if (typeof n == "function") { if (i.has(n)) { console.warn("Cyclic class function detected:", n); continue; } i.add(n); try { const r = n(s); r && e.push(r); } catch (r) { r instanceof Error ? g(`Error resolving class function: ${r.message}`) : g(`Error resolving class function: ${String(r)}`); } } else process.env.NODE_ENV !== "production" && console.warn("Unsupported class input type:", typeof n, "Value:", n); } return Array.from(t); }; function P(a) { return a ? new Set( Object.entries(a).filter(([, s]) => s === !0).map(([s]) => s) ) : /* @__PURE__ */ new Set(); } const v = (a) => typeof a == "string" || Array.isArray(a) || typeof a == "function", B = (a, s = {}) => { const t = { ...a }; for (const e in s) s[e] != null && (t[e] = s[e]); return t; }; class x { constructor() { /** * Internal list of registered plugins. This array is not sorted by default. * Sorting occurs later in the build process. */ o(this, "plugins", []); } /** * Registers a single plugin in the registry, replacing any existing plugin with the same ID. * @param plugin - The plugin to register. * @example * ```ts * globalPlugins.register({ * id: 'my-plugin', * stage: 'before', * order: 10, * apply(builder) { * builder.addBaseClasses(['my-base-class']) * } * }) * ``` */ register(s) { !s.id && process.env.NODE_ENV !== "production" && g("Plugin must have a non-empty id."), this.plugins.findIndex((e) => e.id === s.id) !== -1 && (console.debug(`Plugin with id '${s.id}' is being replaced.`), this.unregister(s.id)), this.plugins.push(s); } /** * Unregisters a plugin by its unique ID. * @param id - The plugin ID to remove. */ unregister(s) { const t = this.plugins.findIndex((e) => e.id === s); t !== -1 && this.plugins.splice(t, 1); } /** * Registers multiple plugins at once, applying the same logic as `register` for each. * @param plugins - Array of plugin objects to register. * @example * ```ts * globalPlugins.registerBatch([ * { * id: 'plugin-a', * stage: 'before', * apply(builder) { * builder.addBaseClasses(['plugin-a-class']) * } * }, * { * id: 'plugin-b', * stage: 'after', * order: -1, * apply(builder) { * builder.addBaseClasses(['plugin-b-class']) * } * } * ]) * ``` */ registerBatch(s) { for (const t of s) this.register(t); } /** * Retrieves all registered plugins, in the order they were inserted. * Final sorting is handled at build time by `ClassNameBuilder`. * @returns An array of plugins. */ getPlugins() { return [...this.plugins]; } /** * Returns all plugins that match a specific stage ('before', 'core', or 'after'), * without sorting by `order`. * @param stage - The stage to filter plugins by. * @returns A filtered list of plugins for the given stage. */ getByStage(s) { return this.plugins.filter((t) => t.stage === s); } /** * Removes all plugins from the registry. * @example * ```ts * globalPlugins.clear() * // now the registry is empty * ``` */ clear() { this.plugins = []; } } const j = new x(); class k { /** * Constructs a new `BaseClassNamePlugin`. * @param order - Numeric order within the 'core' stage (default = 0). */ constructor(s = 0) { /** * A unique identifier for this plugin to help with debugging or HMR consistency. */ o(this, "id", "arto/Internal/AddBaseClassesPlugin"); /** * The plugin stage; defaults to `'core'`, meaning it runs alongside Arto's internal logic. */ o(this, "stage", "core"); /** * The priority order within the `'core'` stage; lower values run first. * @default 0 */ o(this, "order"); this.order = s; } /** * Applies the top-level `className` from the Arto configuration to the builder's base classes, * after normalizing it with `normalizeClassName`. * * @param builder - The `ClassNameBuilder` that accumulates class names from all plugins. */ apply(s) { const t = s.getArtoConfig(), e = s.getContext(); if (t.className) { const i = h(t.className, e); s.addBaseClasses(i); } } } const m = (a) => typeof a == "object" && a !== null && "className" in a, I = (a) => m(a) ? a.className : v(a) ? a : g("Invalid configuration for className. Expected ClassName or StateConfig."), y = (a, s, t) => a ? typeof a == "function" ? a(s, t) : a.every((e) => { var i; return typeof e == "string" ? s.has(e) : !((i = e.not) != null && i.some((n) => s.has(n))); }) : !0; class O { /** * Constructs a new `StatesPlugin` that applies classes for top-level (global) states. * * @param stateConfigs - A mapping of state names to either class names or `StateConfig`. * @param order - Plugin execution priority within the 'core' stage (default = 0). */ constructor(s, t = 0) { o(this, "id", "arto/Internal/ApplyStateClassesPlugin"); o(this, "stage", "core"); o(this, "order"); /** * A map of state keys to either simple class names or detailed `StateConfig`. * For each active state, this plugin checks the config, verifies dependencies, * and merges the resulting class names into the builder. */ o(this, "stateConfigs"); this.stateConfigs = s, this.order = t; } /** * Called automatically by Arto during the plugin lifecycle. Checks each active state: * 1. If a `StateConfig` is found, verify `dependsOn`. * 2. Normalize the class names via `normalizeClassName`. * 3. Add those class names to the builder's global state classes. * * @param builder - The `ClassNameBuilder` handling class name aggregation. */ apply(s) { const t = s.getActiveStates(), e = s.getContext(); for (const i of t) { const n = this.stateConfigs[i]; if (!n || m(n) && !y(n.dependsOn, t, e)) continue; const r = h(I(n), e); s.addGlobalStateClasses(i, r); } } } function E(a) { if (typeof a != "object" || a === null) return !1; const s = a; return "className" in s || "states" in s; } class D { /** * Constructs a `VariantsPlugin` with an optional `order`. * @param order - Plugin priority in the 'core' stage (default = 0). */ constructor(s = 0) { /** * Unique ID for the plugin to help with debugging or HMR consistency. */ o(this, "id", "arto/Internal/ApplyVariantClassesPlugin"); /** * Runs at the 'core' stage by default. */ o(this, "stage", "core"); /** * The order (priority) among 'core' plugins. Lower means earlier execution. * @default 0 */ o(this, "order"); this.order = s; } /** * Called automatically by the builder. Iterates through each variant key in the Arto config, * checks the user's chosen value for that variant, and applies the appropriate class names. * * @param builder - The `ClassNameBuilder` to which classes are added. */ apply(s) { const t = s.getArtoConfig(), e = s.getSelectedVariants(), i = s.getContext(); if (!t.variants) return; const n = Object.keys(t.variants); for (const r of n) { const l = e[r]; if (l == null) continue; const f = t.variants[r]; if (!f) continue; const c = f[l]; c || g( `Invalid value '${String(l)}' for variant '${String(r)}'.` ); const u = this.processVariantConfig( r, c, s, i ); s.addVariantClasses(r, u); } } /** * Determines how to handle a specific variant config: * 1) If it's a direct `ClassName` (string, array, or function), normalize and return it. * 2) If it's a `VariantConfig`, process the base `className` and then any nested states. * 3) Otherwise, throw an error. * * @param variantKey - The variant key (e.g., 'size', 'color'). * @param variantConfig - The config for the chosen variant value. * @param builder - The `ClassNameBuilder` instance. * @param context - Optional context object. * @returns A string array of normalized classes for this variant. */ processVariantConfig(s, t, e, i) { if (v(t)) return h(t, i); if (E(t)) { const n = t.className ? h(t.className, i) : []; return t.states && this.mergeVariantStates(s, t.states, e, i), n; } return g("Invalid variant configuration item encountered."), []; } /** * Merges any variant-level states by iterating through each state definition. * If a state is valid (either a direct class name or a `StateConfig`), the resulting * classes are stored in the builder's `variantStateClasses`. * * @param variantKey - The key of the variant whose states we're processing. * @param statesObj - The object containing state definitions for this variant. * @param builder - The `ClassNameBuilder` instance to store classes. * @param context - Optional context object for callbacks or state checks. */ mergeVariantStates(s, t, e, i) { const n = e.getActiveStates(); for (const [r, l] of Object.entries(t)) { if (!l) continue; const f = r; if (v(l)) { const c = h(l, i); e.addVariantStateClasses(s, f, c); } else if (m(l)) { if (!y(l.dependsOn, n, i)) continue; const c = h(l.className, i); e.replaceVariantStateClasses(s, f, c); } else g(`Invalid state config for state '${r}' ...`); } } } function N(a, s) { switch (s ?? "AND") { case "AND": return a.every(Boolean); case "OR": return a.some(Boolean); case "NOT": return a.every((e) => !e); case "XOR": return a.filter(Boolean).length === 1; case "IMPLIES": return a.length < 2 ? !0 : !a[0] || a[1]; default: return a.every(Boolean); } } function w(a, s, t) { const { variants: e = "AND", states: i = "AND", combine: n = "AND" } = t, r = e === "AND" ? a.every(Boolean) : a.some(Boolean), l = i === "AND" ? s.every(Boolean) : s.some(Boolean); return n === "AND" ? r && l : r || l; } class L { /** * @param order - Numeric priority in the 'core' stage. */ constructor(s = 0) { /** * A unique ID for this plugin. Used for debugging or HMR consistency. */ o(this, "id", "arto/Internal/RulesPlugin"); /** * Runs at the 'core' stage by default. Typically assigned a higher `order` * so that state and variant classes are already applied before rules run. */ o(this, "stage", "core"); /** * Plugin priority within the 'core' stage (default = 0). * Often set to a higher value (e.g., 3) in `arto.ts`. */ o(this, "order"); this.order = s; } /** * Main entry point called by the builder. Iterates over each rule in `artoConfig.rules`, * checks whether the rule conditions pass, and if so: * 1) Removes classes (per `rule.remove`). * 2) Adds classes (per `rule.add`). * * @param builder - The builder that manages class buckets for variants, states, etc. */ apply(s) { const t = s.getArtoConfig(); if (!t.rules) return; const e = s.getActiveStates(), i = s.getSelectedVariants(), n = s.getContext(); for (const r of t.rules) { const { when: l, remove: f, add: c } = r; if (this.doesRuleApply(l, i, e, n) && (f && this.removeStuff(s, f), c)) { const u = h(c, n); s.addBaseClasses(u); } } } /** * Evaluates whether a rule's `when` conditions are met using variants, states, * and optional logic operations. * * @param when - Specifies which variants and states must match, plus an optional logic definition. * @param selectedVariants - The user's chosen variant values. * @param activeStates - The set of active states. * @param context - Optional context for custom logic callbacks. * @returns `true` if the rule should apply, otherwise `false`. */ doesRuleApply(s, t, e, i) { const n = {}; if (s.variants) { const c = Object.keys(s.variants); for (const u of c) { const p = s.variants[u]; if (!p) continue; const d = t[u]; n[u] = d != null && p.includes(d); } } const r = {}; if (s.states) for (const c of s.states) r[c] = e.has(c); const l = [...Object.values(n), ...Object.values(r)]; if (!s.logic) return N(l, "AND"); if (typeof s.logic == "string") return N(l, s.logic); if (typeof s.logic == "object") { const c = Object.values(n), u = Object.values(r); return w(c, u, s.logic); } const f = { variantMatches: n, stateMatches: r, selectedVariants: t, activeStates: e }; return s.logic(f, i); } /** * Removes classes from the builder according to the `ArtoRuleRemove` settings: * - Clears variant classes for specified variant keys. * - Clears state classes for specified states, possibly with a `statesScope`. * - Clears all base classes if `remove.base` is true. * * @param builder - The ClassNameBuilder managing class buckets. * @param remove - The removal instructions (variant keys, states, base). */ removeStuff(s, t) { if (t.variants) for (const e of t.variants) s.clearVariantClasses(e); if (t.states) { const e = t.statesScope ?? "all"; for (const i of t.states) if ((e === "all" || e === "global") && s.clearGlobalStateClasses(i), e === "all" || e === "variant") for (const [n] of Object.entries(s.getSelectedVariants())) s.clearVariantStateClasses(n, i); } t.base && s.clearBaseClasses(); } } const C = class C { /** * @param artoConfig - The main Arto configuration object describing base classes, variants, states, and rules. * @param selectedVariants - The user-selected variants. Keys match `TVariants`, values are chosen at runtime. * @param activeStates - A set of currently active states (e.g., 'disabled'). * @param context - An optional context object (e.g., user data, environment). * @param plugins - A list of plugins that modify or extend class building logic. */ constructor(s, t, e, i, n) { /** * Base class names, applied before any variant or state classes. */ o(this, "baseClassNames", []); /** * A mapping of each variant key to an array of classes. These classes are applied * after `baseClassNames` and can be overridden by state-level classes or subsequent rules. */ o(this, "variantClassNames", {}); /** * A nested mapping of `[variantKey][stateName] => string[]` for variant-level states. * Classes here are applied after standard variant classes if the corresponding state is active. */ o(this, "variantStateClassNames", {}); /** * A mapping of global state names to an array of classes. Applied if the state is active. * These classes override or supplement existing classes from base or variants. */ o(this, "globalStateClassNames", {}); /** * A set of callbacks to run **after** 'core' plugins but **before** 'after' plugins. */ o(this, "postCoreCallbacks", []); /** * A new set of callbacks that run truly "post-build," i.e. after the 'after' stage. */ o(this, "finalBuildCallbacks", []); /** * An array of all plugins (local + global + internal), unsorted by default. Sorting * occurs by stage -> order when applying them sequentially in `build()`. */ o(this, "allPlugins"); this.artoConfig = s, this.selectedVariants = t, this.activeStates = e, this.context = i; const r = n ?? []; for (const l of r) l.stage || (l.stage = "core"), typeof l.order != "number" && (l.order = 0); r.sort((l, f) => { const c = C.stagePriority[l.stage] - C.stagePriority[f.stage]; return c !== 0 ? c : (l.order ?? 0) - (f.order ?? 0); }), this.allPlugins = r; } /** * Builds and returns the final array of class names by: * 1) Running 'before' plugins. * 2) Running 'core' plugins. * 3) Executing any `postCoreCallbacks`. * 4) Running 'after' plugins. * 5) Executing any `finalBuildCallbacks` * 6) Combining and returning all class buckets. * * @returns The combined list of classes (no deduplication), typically joined by spaces. * * @example * ```ts * const builder = new ClassNameBuilder(config, { size: 'small' }, new Set(['disabled']), ...) * const classArray = builder.build() * // => ['base-class', 'small-class', 'disabled-class'] * ``` */ build() { for (const t of this.allPlugins) t.stage === "before" && t.apply(this); for (const t of this.allPlugins) t.stage === "core" && t.apply(this); for (const t of this.postCoreCallbacks) t(); for (const t of this.allPlugins) t.stage === "after" && t.apply(this); for (const t of this.finalBuildCallbacks) t(); const s = []; s.push(...this.baseClassNames); for (const t in this.variantClassNames) s.push(...this.variantClassNames[t]); for (const t in this.variantStateClassNames) { const e = this.variantStateClassNames[t]; if (e) for (const i of this.activeStates) { const n = e[i]; n && s.push(...n); } } for (const t in this.globalStateClassNames) s.push(...this.globalStateClassNames[t]); return s; } // ------------------------------------------------------------------------- // Callbacks // ------------------------------------------------------------------------- /** * Adds a callback to run immediately after 'core' plugins but before 'after' plugins. * * @param callback - A function to run after the core logic has completed. * * @example * ```ts * builder.addPostCoreCallback(() => { * console.log('Classes have been generated!') * }) * ``` */ addPostCoreCallback(s) { this.postCoreCallbacks.push(s); } /** * Adds a callback to run after all plugins have completed, including 'after' stage. * * @param callback - A function to run after all plugins have completed. * * @example * ```ts * builder.addFinalBuildCallback(() => { * console.log('All classes have been generated!') * }) * ``` */ addFinalBuildCallback(s) { this.finalBuildCallbacks.push(s); } // ------------------------------------------------------------------------- // Base Classes // ------------------------------------------------------------------------- /** * Appends an array of classes to the base bucket. * * @param classNames - An array of class names (e.g. `['btn', 'btn-primary']`). */ addBaseClasses(s) { this.baseClassNames.push(...s); } /** * Clears all previously added base classes. */ clearBaseClasses() { this.baseClassNames = []; } /** * Retrieves a copy of the current base classes for inspection or debugging. * * @returns A new array containing all base class names. */ getBaseClasses() { return [...this.baseClassNames]; } // ------------------------------------------------------------------------- // Variant Classes // ------------------------------------------------------------------------- /** * Appends classes for a specific variant key. * * @param variantKey - The name of the variant (e.g. `'size'`). * @param classNames - The classes to add (e.g. `['text-sm', 'py-1']`). */ addVariantClasses(s, t) { this.variantClassNames[s] || (this.variantClassNames[s] = []), this.variantClassNames[s].push(...t); } /** * Replaces all classes for the given variant with a new array of classes. * * @param variantKey - The variant name to replace. * @param classNames - The new classes. */ replaceVariantClasses(s, t) { this.variantClassNames[s] = [...t]; } /** * Clears any existing classes assigned to a given variant. * * @param variantKey - The variant name. */ clearVariantClasses(s) { this.variantClassNames[s] = []; } /** * Retrieves a read-only copy of the full map of variant classes. * * @returns An object with each variant key mapped to an array of class names. */ getVariantClassMap() { const s = {}; for (const t in this.variantClassNames) s[t] = [...this.variantClassNames[t] ?? []]; return s; } // ------------------------------------------------------------------------- // Global State Classes // ------------------------------------------------------------------------- /** * Adds classes to the global state bucket. If `state` is active, these classes * will be appended to the final output (after variant classes). * * @param state - The state name (e.g. `'disabled'`). * @param classNames - The classes to add (e.g. `['opacity-50', 'pointer-events-none']`). */ addGlobalStateClasses(s, t) { this.globalStateClassNames[s] || (this.globalStateClassNames[s] = []), this.globalStateClassNames[s].push(...t); } /** * Replaces existing global state classes for the given `state` with a new array. * * @param state - The state name. * @param classNames - The new array of classes. */ replaceGlobalStateClasses(s, t) { this.globalStateClassNames[s] = [...t]; } /** * Clears any global state classes registered under the specified `state`. * * @param state - The state name to clear (e.g., 'hover'). */ clearGlobalStateClasses(s) { this.globalStateClassNames[s] = []; } /** * Returns a copy of the classes for a specific global state, for inspection or debugging. * * @param state - The state name. * @returns An array of class names, which may be empty if none are set. */ getGlobalStateClassesFor(s) { return [...this.globalStateClassNames[s] ?? []]; } // ------------------------------------------------------------------------- // Variant-Level State Classes // ------------------------------------------------------------------------- /** * Appends classes for a combination of variant key + state name. * These will only be applied if the corresponding variant value is selected and the state is active. * * @param variantKey - The variant key (e.g. 'size'). * @param state - The state name (e.g. 'disabled'). * @param classNames - An array of classes to add. */ addVariantStateClasses(s, t, e) { this.variantStateClassNames[s] || (this.variantStateClassNames[s] = {}), this.variantStateClassNames[s][t] || (this.variantStateClassNames[s][t] = []), this.variantStateClassNames[s][t].push(...e); } /** * Replaces all classes for a specific `(variantKey, state)` pair. * * @param variantKey - The variant key. * @param state - The state name. * @param classNames - The new array of classes. */ replaceVariantStateClasses(s, t, e) { this.variantStateClassNames[s] || (this.variantStateClassNames[s] = {}), this.variantStateClassNames[s][t] = [...e]; } /** * Clears all classes for a specific `(variantKey, state)` pair. * * @param variantKey - The variant key to clear. * @param state - The state name to clear. */ clearVariantStateClasses(s, t) { this.variantStateClassNames[s] && (this.variantStateClassNames[s][t] = []); } /** * Retrieves classes for a given `(variantKey, state)` combination. * * @param variantKey - The variant key (e.g. 'color'). * @param state - The state name (e.g. 'hover'). * @returns An array of classes if they exist; otherwise an empty array. */ getVariantStateClasses(s, t) { var e; return (e = this.variantStateClassNames[s]) != null && e[t] ? [...this.variantStateClassNames[s][t]] : []; } // ------------------------------------------------------------------------- // Additional Getters & Setters // ------------------------------------------------------------------------- /** * Returns the original Arto configuration provided to this builder. * * @returns A readonly version of `ArtoConfig`. */ getArtoConfig() { return this.artoConfig; } /** * Retrieves the user-selected variants (merged with any default variants). * * @returns An object mapping each variant key to the selected value. */ getSelectedVariants() { return this.selectedVariants; } /** * Replaces the current selection of variants with a new mapping. * * @param variants - The new variant key/value mapping. */ setSelectedVariants(s) { this.selectedVariants = s; } /** * Retrieves the set of currently active states. * * @returns A `Set` of active state names. */ getActiveStates() { return this.activeStates; } /** * Replaces the set of active states. * * @param states - The new set of states (e.g. `new Set(['disabled', 'hover'])`). */ setActiveStates(s) { this.activeStates = s; } /** * Returns the optional context object, if any, used by plugins or class generation callbacks. */ getContext() { return this.context; } /** * Retrieves all classes from base, variants, global states, and variant-level states. * * @returns An array containing all classes. */ getAllClasses() { const s = []; s.push(...this.baseClassNames); for (const t in this.variantClassNames) s.push(...this.variantClassNames[t]); for (const t of this.activeStates) s.push(...this.getGlobalStateClassesFor(t)); for (const t of Object.keys(this.selectedVariants)) for (const e of this.activeStates) { const i = this.getVariantStateClasses(t, e); s.push(...i); } return s; } }; /** * Maps each plugin stage to a numeric value used for sorting: * - 'before' -> 0 * - 'core' -> 1 * - 'after' -> 2 */ o(C, "stagePriority", { before: 0, core: 1, after: 2 }); let S = C; const $ = (a, s) => ((!a || typeof a != "object") && g("Invalid config provided to arto."), (t) => { const e = (t == null ? void 0 : t.variants) ?? {}, i = a.defaultVariants ?? {}, n = (t == null ? void 0 : t.states) ?? {}, r = t == null ? void 0 : t.context, l = B(i, e), f = P(n), c = [...s || [], ...j.getPlugins()], u = []; a.className && u.push(new k()), a.variants && u.push(new D()), a.states && u.push(new O(a.states)), a.rules && a.rules.length > 0 && u.push(new L(3)); const p = [...c, ...u]; return new S( a, l, f, r, p ).build().join(" "); }); export { S as ClassNameBuilder, x as PluginRegistry, $ as arto, h as normalizeClassName, j as pluginHub };