UNPKG

@vitus-labs/core

Version:
569 lines (558 loc) 15.1 kB
import { cloneElement, createContext, createElement, isValidElement, useMemo, useRef } from "react"; import { Fragment, jsx } from "react/jsx-runtime"; import { isFragment, isMemo, isValidElementType } from "react-is"; //#region src/compose.ts /** * Right-to-left function composition. * `compose(f, g, h)(x)` === `f(g(h(x)))`. * * Used throughout the system to build HOC chains — * `compose(attrsHoc, userHoc1, userHoc2)(Component)`. */ const compose = (...fns) => (p) => fns.reduceRight((acc, cur) => cur(acc), p); //#endregion //#region src/config.ts const notConfigured = (_name) => { throw new Error("[@vitus-labs/core] CSS engine not configured. Call init() with a connector before rendering.\n\n import { init } from '@vitus-labs/core'\n import * as connector from '@vitus-labs/connector-styler'\n init(connector)\n"); }; const createStyledDelegate = (self) => { const createLazy = (tag, options, strings, values) => { let Real = null; const Lazy = (props) => { if (!Real) { const engine = self._styled; if (!engine) return notConfigured("styled"); Real = options ? engine(tag, options)(strings, ...values) : engine(tag)(strings, ...values); } return createElement(Real, props); }; Lazy.displayName = `styled(${typeof tag === "string" ? tag : tag.displayName || tag.name || "Component"})`; return Lazy; }; const factory = (tag, options) => { return (strings, ...values) => { if (self._styled) return options ? self._styled(tag, options)(strings, ...values) : self._styled(tag)(strings, ...values); return createLazy(tag, options, strings, values); }; }; return new Proxy(factory, { get(_target, prop) { if (prop === "prototype" || prop === "$$typeof") return void 0; return (strings, ...values) => { if (self._styled) return self._styled[prop](strings, ...values); return createLazy(prop, void 0, strings, values); }; } }); }; /** * Singleton configuration that bridges the UI system with the chosen * CSS-in-JS engine. All packages reference `config.css`, `config.styled`, * etc. — the engine is swapped via `init()` with a connector. * * The `css` and `styled` properties are **stable delegate functions** that * can be safely destructured at module level (`const { styled, css } = config`). * They internally read the latest engine reference set by `init()`. * * When destructured before `init()` is called: * - `css` returns a thunk (function) that resolves at render time * - `styled` returns a lazy component that creates the real component on first render */ var Configuration = class { _css = null; _styled = null; _provider = null; _keyframes = null; _createGlobalStyle = null; _useTheme = null; component = "div"; textComponent = "span"; createMediaQueries = void 0; /** * Stable CSS delegate. When the engine is available, delegates immediately. * When not (module load time before init), returns a thunk that resolves * at render time — all CSS-in-JS engines treat functions as interpolations. */ css = (strings, ...values) => { if (this._css) return this._css(strings, ...values); return (() => { const engine = this._css; if (!engine) return notConfigured("css"); return engine(strings, ...values); }); }; /** * Stable styled delegate (Proxy). Supports `styled(tag)` and `styled.div`. * When the engine is not yet available, returns a lazy component * that creates the real styled component on first render. */ styled; /** The external ThemeProvider from the configured engine. */ get ExternalProvider() { return this._provider; } /** Keyframes factory from the configured engine, or null. */ get keyframes() { return this._keyframes ?? null; } /** Global style factory from the configured engine, or null. */ get createGlobalStyle() { return this._createGlobalStyle ?? null; } /** Theme hook from the configured engine, or null. */ get useTheme() { return this._useTheme ?? null; } constructor() { this.styled = createStyledDelegate(this); } /** * Initialize or swap the CSS-in-JS engine. Must be called before any * component renders (typically at the app entry point). * * @example * ```typescript * import { init } from '@vitus-labs/core' * import * as connector from '@vitus-labs/connector-styler' * init(connector) * ``` */ init = (props) => { if (props.css) this._css = props.css; if (props.styled) this._styled = props.styled; if (props.provider) this._provider = props.provider; if (props.keyframes) this._keyframes = props.keyframes; if (props.createGlobalStyle) this._createGlobalStyle = props.createGlobalStyle; if (props.useTheme) this._useTheme = props.useTheme; if (props.component) this.component = props.component; if (props.textComponent) this.textComponent = props.textComponent; if (props.createMediaQueries) this.createMediaQueries = props.createMediaQueries; }; }; const config = new Configuration(); const { init } = config; //#endregion //#region src/isEmpty.ts const isEmpty = (param) => { if (!param) return true; if (typeof param !== "object") return true; if (Array.isArray(param)) return param.length === 0; return Object.keys(param).length === 0; }; //#endregion //#region src/isEqual.ts const isArrayEqual = (a, b, seen) => { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) if (!isEqualInner(a[i], b[i], seen)) return false; return true; }; const isObjectEqual = (a, b, seen) => { const aKeys = Object.keys(a); if (aKeys.length !== Object.keys(b).length) return false; for (const key of aKeys) { if (!Object.hasOwn(b, key)) return false; if (!isEqualInner(a[key], b[key], seen)) return false; } return true; }; const isEqualInner = (a, b, seen) => { if (Object.is(a, b)) return true; if (typeof a !== typeof b || a == null || b == null || typeof a !== "object") return false; const aObj = a; const bObj = b; let bSet = seen.get(aObj); if (bSet !== void 0) { if (bSet.has(bObj)) return true; bSet.add(bObj); } else { bSet = /* @__PURE__ */ new WeakSet(); bSet.add(bObj); seen.set(aObj, bSet); } if (Array.isArray(a)) return Array.isArray(b) && isArrayEqual(a, b, seen); if (Array.isArray(b)) return false; return isObjectEqual(a, b, seen); }; const isEqual = (a, b) => isEqualInner(a, b, /* @__PURE__ */ new WeakMap()); //#endregion //#region src/useStableValue.ts /** * Returns a referentially stable version of `value`. The returned reference * only changes when the value is no longer deeply equal to the previous one. * * Use this to stabilize object/array props before passing them as hook * dependencies, preventing unnecessary recalculations in useMemo/useEffect. * * Based on the useDeepCompareMemoize pattern from use-deep-compare. */ const useStableValue = (value) => { const ref = useRef(value); if (!isEqual(ref.current, value)) ref.current = value; return ref.current; }; //#endregion //#region src/context.tsx /** * Internal React context shared across all @vitus-labs packages. * Carries the theme object plus any extra provider props. */ const context = createContext({}); const VitusLabsProvider = context.Provider; /** * Dual-layer provider that feeds both the internal VitusLabs context * and an optional external styling provider (e.g. styled-components' * ThemeProvider). When no theme is supplied, renders children directly. */ const Provider = ({ theme, children, ...props }) => { const ExternalProvider = useMemo(() => config.ExternalProvider, []); const context = useStableValue({ theme, ...props }); if (isEmpty(theme) || !theme) return /* @__PURE__ */ jsx(Fragment, { children }); if (ExternalProvider) return /* @__PURE__ */ jsx(VitusLabsProvider, { value: context, children: /* @__PURE__ */ jsx(ExternalProvider, { theme, children }) }); return /* @__PURE__ */ jsx(VitusLabsProvider, { value: context, children }); }; //#endregion //#region src/hoistNonReactStatics.ts const REACT_STATICS = { childContextTypes: true, contextType: true, contextTypes: true, defaultProps: true, displayName: true, getDefaultProps: true, getDerivedStateFromError: true, getDerivedStateFromProps: true, mixins: true, propTypes: true, type: true }; const KNOWN_STATICS = { name: true, length: true, prototype: true, caller: true, callee: true, arguments: true, arity: true }; const FORWARD_REF_STATICS = { $$typeof: true, render: true, defaultProps: true, displayName: true, propTypes: true }; const MEMO_STATICS = { $$typeof: true, compare: true, defaultProps: true, displayName: true, propTypes: true, type: true }; const TYPE_STATICS = {}; TYPE_STATICS[Symbol.for("react.forward_ref")] = FORWARD_REF_STATICS; TYPE_STATICS[Symbol.for("react.memo")] = MEMO_STATICS; const getStatics = (component) => { if (isMemo(component)) return MEMO_STATICS; return TYPE_STATICS[component.$$typeof] || REACT_STATICS; }; const hoistNonReactStatics = (target, source, excludeList) => { if (typeof source === "string") return target; const proto = Object.getPrototypeOf(source); if (proto && proto !== Object.prototype) hoistNonReactStatics(target, proto, excludeList); const keys = [...Object.getOwnPropertyNames(source), ...Object.getOwnPropertySymbols(source)]; const targetStatics = getStatics(target); const sourceStatics = getStatics(source); for (const key of keys) { const k = key; if (KNOWN_STATICS[k] || excludeList?.[k] || sourceStatics[k] || targetStatics[k]) continue; const descriptor = Object.getOwnPropertyDescriptor(source, key); if (descriptor) try { Object.defineProperty(target, key, descriptor); } catch {} } return target; }; //#endregion //#region src/html/htmlTags.ts const HTML_TAGS = [ "a", "abbr", "address", "area", "article", "aside", "audio", "b", "bdi", "bdo", "big", "blockquote", "body", "br", "button", "canvas", "caption", "cite", "code", "col", "colgroup", "data", "datalist", "dd", "del", "details", "dfn", "dialog", "div", "dl", "dt", "em", "embed", "fieldset", "figcaption", "figure", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hr", "html", "i", "iframe", "img", "input", "ins", "kbd", "label", "legend", "li", "main", "map", "mark", "meter", "nav", "object", "ol", "optgroup", "option", "output", "p", "picture", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "section", "select", "small", "source", "span", "strong", "sub", "summary", "sup", "svg", "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "time", "tr", "track", "u", "ul", "var", "video", "wbr" ]; const HTML_TEXT_TAGS = [ "abbr", "b", "bdi", "bdo", "big", "blockquote", "cite", "code", "del", "div", "dl", "dt", "em", "figcaption", "h1", "h2", "h3", "h4", "h5", "h6", "i", "ins", "kbd", "label", "legend", "li", "p", "pre", "q", "rp", "rt", "s", "small", "span", "strong", "sub", "summary", "sup", "time", "u" ]; //#endregion //#region src/render.ts const render = (content, attachProps) => { if (!content) return null; const render = (child) => attachProps ? createElement(child, attachProps) : createElement(child); const t = typeof content; if (t === "string" || t === "number" || t === "boolean" || t === "bigint") return content; if (Array.isArray(content) || isFragment(content)) return content; if (isValidElementType(content)) return render(content); if (isValidElement(content)) { if (isEmpty(attachProps)) return content; return cloneElement(content, attachProps); } return content; }; //#endregion //#region src/utils.ts const omit = (obj, keys) => { if (obj == null) return {}; if (!keys) return { ...obj }; const keysSet = keys instanceof Set ? keys : new Set(keys); if (keysSet.size === 0) return { ...obj }; const result = {}; for (const key in obj) if (Object.hasOwn(obj, key) && !keysSet.has(key)) result[key] = obj[key]; return result; }; const pick = (obj, keys) => { if (obj == null) return {}; if (!keys || keys.length === 0) return { ...obj }; const result = {}; for (const key of keys) { const k = key; if (Object.hasOwn(obj, k)) result[k] = obj[k]; } return result; }; const PATH_RE = /[^.[\]]+/g; /** Split a dot/bracket path string into individual key tokens. */ const parsePath = (path) => { if (Array.isArray(path)) return path; return path.match(PATH_RE) ?? []; }; const isUnsafeKey = (key) => key === "__proto__" || key === "prototype" || key === "constructor"; const get = (obj, path, defaultValue) => { const keys = parsePath(path); let result = obj; for (const key of keys) { if (result == null || isUnsafeKey(key)) return defaultValue; result = result[key]; } return result === void 0 ? defaultValue : result; }; const set = (obj, path, value) => { const keys = parsePath(path); let current = obj; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; if (isUnsafeKey(key)) return obj; const nextKey = keys[i + 1]; if (isUnsafeKey(nextKey)) return obj; if (current[key] == null) current[key] = /^\d+$/.test(nextKey) ? [] : {}; current = current[key]; } const lastKey = keys[keys.length - 1]; if (lastKey != null && !isUnsafeKey(lastKey)) current[lastKey] = value; return obj; }; const throttle = (fn, wait = 0, options) => { const leading = options?.leading !== false; const trailing = options?.trailing !== false; let lastCallTime; let timeoutId; let lastArgs; const invoke = (args) => { lastCallTime = Date.now(); fn(...args); }; const startTrailingTimer = (args, delay) => { lastArgs = args; if (timeoutId !== void 0) return; timeoutId = setTimeout(() => { timeoutId = void 0; if (lastArgs) { invoke(lastArgs); lastArgs = void 0; } }, delay); }; const throttled = (...args) => { const now = Date.now(); const elapsed = lastCallTime === void 0 ? wait : now - lastCallTime; if (elapsed >= wait) if (leading) invoke(args); else { lastCallTime = now; if (trailing) startTrailingTimer(args, wait); } else if (trailing) startTrailingTimer(args, wait - elapsed); }; throttled.cancel = () => { if (timeoutId !== void 0) { clearTimeout(timeoutId); timeoutId = void 0; } lastArgs = void 0; lastCallTime = void 0; }; return throttled; }; const isPlainObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype; const merge = (target, ...sources) => { for (const source of sources) { if (source == null) continue; for (const key of Object.keys(source)) { if (key === "__proto__" || key === "constructor" || key === "prototype") continue; const targetVal = target[key]; const sourceVal = source[key]; if (isPlainObject(targetVal) && isPlainObject(sourceVal)) target[key] = merge({ ...targetVal }, sourceVal); else target[key] = sourceVal; } } return target; }; //#endregion export { HTML_TAGS, HTML_TEXT_TAGS, Provider, compose, config, context, get, hoistNonReactStatics, init, isEmpty, isEqual, merge, omit, pick, render, set, throttle, useStableValue }; //# sourceMappingURL=index.js.map