@elenajs/core 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"bundle.js","sources":["../src/common/props.js","../src/common/events.js","../src/common/utils.js","../src/common/render.js","../src/elena.js"],"sourcesContent":["/**\n * Get the value of the Elena Element property.\n *\n * @param {string} type\n * @param {any} value\n * @param {\"toAttribute\" | \"toProp\"} [transform]\n */\nexport function getPropValue(type, value, transform) {\n value = type === \"boolean\" && typeof value !== \"boolean\" ? value !== null : value;\n\n if (!transform) {\n return value;\n } else if (transform === \"toAttribute\") {\n switch (type) {\n case \"object\":\n case \"array\":\n return value === null ? null : JSON.stringify(value);\n case \"boolean\":\n return value ? \"\" : null;\n case \"number\":\n return value === null ? null : value;\n default:\n return value === \"\" ? null : value;\n }\n } else {\n switch (type) {\n case \"object\":\n case \"array\":\n if (!value) {\n return value;\n }\n try {\n return JSON.parse(value);\n } catch {\n console.warn(\"░█ [ELENA]: Invalid JSON for prop, received: \" + value);\n return null;\n }\n case \"boolean\":\n return value; // conversion already handled above\n case \"number\":\n return value !== null ? +value : value;\n default:\n return value;\n }\n }\n}\n\n/**\n * Set or remove an attribute on an Elena Element.\n *\n * @param {Element} element - Target element\n * @param {string} name - Attribute name\n * @param {string | null} value - Attribute value, or null to remove\n */\nexport function syncAttribute(element, name, value) {\n if (!element) {\n console.warn(\"░█ [ELENA]: Cannot sync attributes to a null element.\");\n return;\n }\n if (value === null) {\n element.removeAttribute(name);\n } else {\n element.setAttribute(name, value);\n }\n}\n\n/**\n * Define prop getters/setters on the prototype once\n * at class-creation time. Values are stored per-instance\n * via a `_props` Map that is lazily created.\n *\n * @param {Function} proto - The class prototype\n * @param {string[]} properties - Prop names to define\n * @param {boolean} [syncToElement=true] - Whether to also sync prop attributes to the\n * inner element ref. Pass false for slot components that have no explicit inner\n * element selector, so props are not leaked onto slotted children.\n */\nexport function setProps(proto, properties, syncToElement = true) {\n for (const prop of properties) {\n Object.defineProperty(proto, prop, {\n configurable: true,\n enumerable: true,\n get() {\n return this._props ? this._props.get(prop) : undefined;\n },\n set(value) {\n if (!this._props) {\n this._props = new Map();\n }\n if (value === this._props.get(prop)) {\n return;\n }\n\n this._props.set(prop, value);\n if (!this.isConnected) {\n return;\n }\n\n const attrValue = getPropValue(typeof value, value, \"toAttribute\");\n syncAttribute(this, prop, attrValue);\n if (syncToElement && this.element) {\n syncAttribute(this.element, prop, attrValue);\n }\n },\n });\n }\n}\n\n/**\n * We need to update the internals of the Elena Element\n * when props on the host element are changed.\n *\n * @param {object} context\n * @param {string} name\n * @param {any} oldValue\n * @param {any} newValue\n */\nexport function getProps(context, name, oldValue, newValue) {\n if (oldValue !== newValue) {\n const type = typeof context[name];\n const newAttr = getPropValue(type, newValue, \"toProp\");\n context[name] = newAttr;\n }\n}\n","/**\n * A base class for Elena Element’s events which\n * defaults to bubbling and composed.\n */\nexport class ElenaEvent extends Event {\n constructor(type, eventInitDict) {\n super(type, {\n bubbles: true,\n composed: true,\n ...eventInitDict,\n });\n }\n}\n","/**\n * Register the Elena Element if the browser supports it.\n *\n * @param {string} tagName\n * @param {Function} Element\n */\nexport function defineElement(tagName, Element) {\n if (typeof window !== \"undefined\" && \"customElements\" in window) {\n if (!window.customElements.get(tagName)) {\n window.customElements.define(tagName, Element);\n }\n }\n}\n\n/**\n * Escape a string for safe insertion into HTML.\n *\n * @param {string} str\n * @returns {string}\n */\nexport function escapeHtml(str) {\n const Escape = { \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\", '\"': \"&quot;\", \"'\": \"&#39;\" };\n return String(str).replace(/[&<>\"']/g, c => Escape[c]);\n}\n\n/**\n * Tagged template for trusted HTML. Use as the return value of render(), or for\n * sub-fragments inside render methods. Interpolated values are auto-escaped;\n * nested `html` fragments are passed through without double-escaping.\n *\n * @param {TemplateStringsArray} strings\n * @param {...*} values\n * @returns {{ __raw: true, strings: TemplateStringsArray, values: Array, toString(): string }}\n */\nexport function html(strings, ...values) {\n const result = strings.reduce((acc, str, i) => {\n const v = values[i];\n return acc + str + (v && v.__raw ? String(v) : escapeHtml(String(v ?? \"\")));\n }, \"\");\n return { __raw: true, strings, values, toString: () => result };\n}\n\n/**\n * A placeholder you can return from a conditional expression inside a template\n * to render nothing. Always produces an empty string; signals to the template\n * engine that no further processing is needed.\n *\n * @type {{ __raw: true, toString(): string }}\n */\nexport const nothing = { __raw: true, toString: () => \"\" };\n","import { escapeHtml } from \"./utils.js\";\n\n/** @type {WeakMap<TemplateStringsArray, string[]>} */\nconst _stringsCache = new WeakMap();\n\n/**\n * Render a tagged template into an Elena Element with DOM diffing.\n *\n * On first render, builds the full HTML markup and render.\n * On re-renders, patches only the text nodes whose values changed,\n * avoiding a full DOM rebuild.\n *\n * Cache state is stored on the element instance:\n * - _tplStrings: reference to the template’s static strings array\n * - _tplValues: array of escaped values from the last render\n * - _tplParts: array mapping each value index to its DOM text node\n *\n * @param {HTMLElement} element\n * @param {TemplateStringsArray} strings - Static parts of the tagged template\n * @param {Array} values - Dynamic interpolated values\n */\nexport function renderTemplate(element, strings, values) {\n if (patchTextNodes(element, strings, values)) {\n return;\n }\n fullRender(element, strings, values);\n}\n\n/**\n * Fast path: patch only the text nodes whose values changed.\n * Returns true if all changes were handled (no full render needed).\n *\n * @param {HTMLElement} element - The host element with cached template state\n * @param {TemplateStringsArray} strings - Static parts of the tagged template\n * @param {Array} values - Dynamic interpolated values\n * @returns {boolean} Whether patching was sufficient (false = full render)\n */\nfunction patchTextNodes(element, strings, values) {\n // Only works when re-rendering the same template shape\n if (element._tplStrings !== strings || !element._tplParts) {\n return false;\n }\n\n let needsFullRender = false;\n\n for (let i = 0; i < values.length; i++) {\n const v = values[i];\n // Check if this value is a trusted HTML fragment (isRaw),\n // reated via the `html` tag, which bypasses escaping\n const isRaw = v && v.__raw;\n const newRendered = isRaw ? String(v) : escapeHtml(String(v ?? \"\"));\n const oldRendered = element._tplValues[i];\n\n if (newRendered === oldRendered) {\n continue;\n }\n\n element._tplValues[i] = newRendered;\n\n // Raw HTML values or attribute-position values require a full render\n if (isRaw) {\n needsFullRender = true;\n } else {\n const textNode = element._tplParts[i];\n if (textNode) {\n // Value is in a text position, update the DOM node directly\n textNode.textContent = String(v ?? \"\");\n } else {\n // Value is in an attribute position, can’t patch, need full render\n needsFullRender = true;\n }\n }\n }\n\n return !needsFullRender;\n}\n\n/**\n * Cold path: build full HTML markup, render it via DocumentFragment,\n * and map each interpolated value to its corresponding DOM text node\n * for future fast-path patching.\n *\n * @param {HTMLElement} element - The host element to render into\n * @param {TemplateStringsArray} strings - Static parts of the tagged template\n * @param {Array} values - Dynamic interpolated values\n */\nfunction fullRender(element, strings, values) {\n const renderedValues = values.map(v => (v && v.__raw ? String(v) : escapeHtml(String(v ?? \"\"))));\n\n // The JS engine reuses the same `strings` object for every call from the same template\n // literal, so cache the cleaned-up parts and run the regex only once per template.\n let processedStrings = _stringsCache.get(strings);\n if (!processedStrings) {\n processedStrings = Array.from(strings, s => s.replace(/\\n\\s*/g, \" \"));\n _stringsCache.set(strings, processedStrings);\n }\n\n // Build the complete HTML markup\n const markup = processedStrings\n .reduce((out, str, i) => out + str + (renderedValues[i] ?? \"\"), \"\")\n .replace(/>\\s+</g, \"><\")\n .replace(/>\\s+/g, \">\")\n .replace(/\\s+</g, \"<\")\n .trim();\n\n renderHtml(element, markup);\n\n // Cache template identity and rendered values\n element._tplStrings = strings;\n element._tplValues = renderedValues;\n\n // Walk text nodes to map each value index to its DOM node\n element._tplParts = mapTextNodes(element, renderedValues);\n}\n\n/**\n * Walk the Elena Element’s text nodes and map each escaped value\n * to its corresponding DOM text node. Values in attribute\n * positions won’t have a matching text node and will be null.\n *\n * Known limitation: text nodes are matched by content. This could\n * cause the wrong node to be patched if a static part of the template\n * contains text that exactly matches a dynamic value with no surrounding\n * tag. In practice this is extremely rare, static template parts should\n * be HTML structure (tags, attributes, punctuation), not raw text.\n *\n * Example: `<span>Elena</span>${name}` with `name = \"Elena\"`. The\n * walker matches the static \"Elena\" inside the span first, so when\n * `name` later changes to \"Bob\", the span text is patched instead\n * of the trailing text node.\n *\n * @param {HTMLElement} element - The host element to walk\n * @param {string[]} escapedValues - HTML-escaped interpolated values\n * @returns {Array<Text | undefined>} Array mapping each value index to its text node\n */\nfunction mapTextNodes(element, escapedValues) {\n const parts = new Array(escapedValues.length);\n const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);\n\n let valueIndex = 0;\n let node;\n\n while ((node = walker.nextNode()) && valueIndex < escapedValues.length) {\n if (node.textContent === escapedValues[valueIndex]) {\n parts[valueIndex] = node;\n valueIndex++;\n }\n }\n\n return parts;\n}\n\n/**\n * Render an HTML string into an element by parsing it into a\n * DocumentFragment via createContextualFragment and swapping\n * the element’s children in a single replaceChildren call.\n *\n * Uses element.ownerDocument.createRange() so that elements\n * in e.g. iframes parse HTML correctly.\n *\n * @param {HTMLElement} element\n * @param {string} markup\n */\nexport function renderHtml(element, markup) {\n if (!element) {\n console.warn(\"░█ [ELENA]: Cannot render to a null element.\");\n return;\n }\n element.replaceChildren(element.ownerDocument.createRange().createContextualFragment(markup));\n}\n","/**\n * ██████████ ████\n * ░░███░░░░░█░░███\n * ░███ █ ░ ░███ ██████ ████████ ██████\n * ░██████ ░███ ███░░███░░███░░███ ░░░░░███\n * ░███░░█ ░███ ░███████ ░███ ░███ ███████\n * ░███ ░ █ ░███ ░███░░░ ░███ ░███ ███░░███\n * ██████████ █████░░██████ ████ █████░░████████\n * ░░░░░░░░░░ ░░░░░ ░░░░░░ ░░░░ ░░░░░ ░░░░░░░░\n *\n * Elena: Progressive Web Components\n * https://elenajs.com\n */\n\nimport { setProps, getProps, getPropValue, syncAttribute } from \"./common/props.js\";\nimport { ElenaEvent } from \"./common/events.js\";\nimport { defineElement, html, nothing } from \"./common/utils.js\";\nimport { renderTemplate } from \"./common/render.js\";\n\nexport { html, nothing };\n\n/**\n * @typedef {Object} ElenaOptions\n * @property {string} [tagName] - Custom element tag name to register (e.g. \"elena-button\").\n * @property {string[]} [props] - Props observed and synced as attributes.\n * @property {string[]} [events] - Events to delegate from the inner element.\n * @property {string} [element] - CSS selector for the inner element.\n */\n\n/**\n * @typedef {new (...args: any[]) => HTMLElement} ElenaConstructor\n */\n\n/**\n * Factory that creates Elena mixin class.\n *\n * Wraps `superClass` with Elena’s lifecycle, templating, props,\n * and events features. The `options` argument is optional.\n *\n * @param {ElenaConstructor} superClass - Base class to extend.\n * @param {ElenaOptions} [options] - Optional configuration options.\n * @returns {CustomElementConstructor} A class ready to be registered.\n */\nexport function Elena(superClass, options) {\n /**\n * Pre-compile element resolver once at class definition\n * time to improve performance:\n *\n * 1. no selector: firstElementChild (property access)\n * 2. className: getElementsByClassName (skips full selector parser)\n * 3. any other: querySelector (full parser, only when needed)\n */\n const resolveElement = !(options && options.element)\n ? host => host.firstElementChild\n : /^[a-z][a-z0-9-]*$/i.test(options.element)\n ? host => host.getElementsByClassName(options.element)[0]\n : host => host.querySelector(options.element);\n\n /**\n * Set up the initial state and default values for Elena Element.\n */\n class ElenaElement extends superClass {\n /**\n * Reference to the base element in the provided template.\n *\n * @type {Object}\n */\n element = null;\n\n /**\n * This method is called when the Elena Element’s\n * props are changed, added, removed or replaced.\n *\n * @param {string} prop\n * @param {string} oldValue\n * @param {string} newValue\n */\n attributeChangedCallback(prop, oldValue, newValue) {\n getProps(this, prop, oldValue, newValue);\n\n // Re-render when attributes change (after initial render).\n // Guard against re-entrant renders: if render() itself mutates an observed\n // attribute, skip the recursive call to prevent an infinite loop.\n // Use _hydrated (set at end of connectedCallback) rather than this.element\n // so slot components without an inner element ref also re-render correctly.\n if (this._hydrated && oldValue !== newValue && !this._isRendering) {\n this._isRendering = true;\n this._applyRender();\n this._isRendering = false;\n }\n }\n\n /**\n * This static method returns an array containing the\n * names of the props we want to observe.\n */\n static get observedAttributes() {\n return options && options.props ? options.props : [];\n }\n\n /**\n * Override in a subclass to define the element's HTML structure.\n * Return an `html` tagged template literal.\n * No-op by default: elements without a render method connect safely.\n */\n render() {}\n\n /**\n * @internal\n */\n _applyRender() {\n const result = this.render();\n if (result && result.strings) {\n renderTemplate(this, result.strings, result.values);\n }\n }\n\n /**\n * This method is called each time the Elena Element\n * is added to the document.\n */\n connectedCallback() {\n this._applyRender();\n\n if (!this.element) {\n this.element = resolveElement(this);\n\n if (!this.element) {\n // Only warn when an explicit element selector was provided but didn't match.\n // Slot components (no element option) intentionally have no inner ref.\n if (options && options.element) {\n console.warn(\"░█ [ELENA]: No element found, using firstElementChild as fallback.\");\n }\n this.element = this.firstElementChild;\n }\n }\n\n // Flush props set before connection to host and inner element attributes\n if (this._props) {\n const hasElementSelector = !!(options && options.element);\n for (const [prop, value] of this._props) {\n const attrValue = getPropValue(typeof value, value, \"toAttribute\");\n syncAttribute(this, prop, attrValue);\n if (hasElementSelector) {\n syncAttribute(this.element, prop, attrValue);\n }\n }\n }\n\n if (!this._events && options && options.events) {\n this._events = true;\n options.events?.forEach(e => {\n this.element.addEventListener(e, this);\n this[e] = (...args) => this.element[e](...args);\n });\n }\n\n this.updated();\n }\n\n /**\n * Perform post-update after each render().\n *\n * @internal\n */\n updated() {\n if (!this._hydrated) {\n this._hydrated = true;\n this.classList.add(\"elena-hydrated\");\n }\n }\n\n /**\n * This method is called each time the Elena Element\n * is removed from the document.\n */\n disconnectedCallback() {\n if (this._events) {\n this._events = false;\n options.events?.forEach(e => {\n this.element?.removeEventListener(e, this);\n });\n }\n }\n\n /**\n * Handles events on the Elena Element.\n *\n * @internal\n */\n handleEvent(event) {\n if (options.events?.includes(event.type)) {\n event.stopPropagation();\n /** @internal */\n this.dispatchEvent(new ElenaEvent(event.type, { cancelable: true }));\n }\n }\n }\n\n if (options && options.props?.length) {\n setProps(ElenaElement.prototype, options.props, !!(options && options.element));\n }\n\n if (options && options.tagName) {\n /** @type {string} */\n ElenaElement._tagName = options.tagName;\n }\n\n /**\n * Register this class as a custom element using the `tagName`\n * set in options. Must be called on the final subclass, not\n * on the Elena mixin directly.\n *\n * @this {CustomElementConstructor}\n */\n ElenaElement.define = function () {\n if (this._tagName) {\n defineElement(this._tagName, this);\n }\n };\n\n return ElenaElement;\n}\n"],"names":["getPropValue","type","value","transform","JSON","stringify","parse","console","warn","syncAttribute","element","name","removeAttribute","setAttribute","ElenaEvent","Event","constructor","eventInitDict","super","bubbles","composed","escapeHtml","str","Escape","String","replace","c","html","strings","values","result","reduce","acc","i","v","__raw","toString","nothing","_stringsCache","WeakMap","renderTemplate","_tplStrings","_tplParts","needsFullRender","length","isRaw","newRendered","_tplValues","textNode","textContent","patchTextNodes","renderedValues","map","processedStrings","get","Array","from","s","set","markup","out","trim","replaceChildren","ownerDocument","createRange","createContextualFragment","renderHtml","escapedValues","parts","walker","document","createTreeWalker","NodeFilter","SHOW_TEXT","node","valueIndex","nextNode","mapTextNodes","fullRender","Elena","superClass","options","resolveElement","test","host","getElementsByClassName","querySelector","firstElementChild","ElenaElement","attributeChangedCallback","prop","oldValue","newValue","context","newAttr","getProps","this","_hydrated","_isRendering","_applyRender","observedAttributes","props","render","connectedCallback","_props","hasElementSelector","attrValue","_events","events","forEach","e","addEventListener","args","updated","classList","add","disconnectedCallback","removeEventListener","handleEvent","event","includes","stopPropagation","dispatchEvent","cancelable","proto","properties","syncToElement","Object","defineProperty","configurable","enumerable","undefined","Map","isConnected","setProps","prototype","tagName","_tagName","define","Element","window","customElements"],"mappings":"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"}
1
+ {"version":3,"file":"bundle.js","sources":["../src/common/props.js","../src/common/events.js","../src/common/utils.js","../src/common/render.js","../src/elena.js"],"sourcesContent":["/**\n * Get the value of the Elena Element property.\n *\n * @param {string} type\n * @param {any} value\n * @param {\"toAttribute\" | \"toProp\"} [transform]\n */\nexport function getPropValue(type, value, transform) {\n value = type === \"boolean\" && typeof value !== \"boolean\" ? value !== null : value;\n\n if (!transform) {\n return value;\n } else if (transform === \"toAttribute\") {\n switch (type) {\n case \"object\":\n case \"array\":\n return value === null ? null : JSON.stringify(value);\n case \"boolean\":\n return value ? \"\" : null;\n case \"number\":\n return value === null ? null : value;\n default:\n return value === \"\" ? null : value;\n }\n } else {\n switch (type) {\n case \"object\":\n case \"array\":\n if (!value) {\n return value;\n }\n try {\n return JSON.parse(value);\n } catch {\n console.warn(\"░█ [ELENA]: Invalid JSON for prop, received: \" + value);\n return null;\n }\n case \"boolean\":\n return value; // conversion already handled above\n case \"number\":\n return value !== null ? +value : value;\n default:\n return value;\n }\n }\n}\n\n/**\n * Set or remove an attribute on an Elena Element.\n *\n * @param {Element} element - Target element\n * @param {string} name - Attribute name\n * @param {string | null} value - Attribute value, or null to remove\n */\nexport function syncAttribute(element, name, value) {\n if (!element) {\n console.warn(\"░█ [ELENA]: Cannot sync attributes to a null element.\");\n return;\n }\n if (value === null) {\n element.removeAttribute(name);\n } else {\n element.setAttribute(name, value);\n }\n}\n\n/**\n * Define prop getters/setters on the prototype once\n * at class-creation time. Values are stored per-instance\n * via a `_props` Map that is lazily created.\n *\n * @param {Function} proto - The class prototype\n * @param {string[]} properties - Prop names to define\n * @param {boolean} [syncToElement=true] - Whether to also sync prop attributes to the\n * inner element ref. Pass false for Composite Components that have no explicit inner\n * element selector, so props are not leaked onto slotted children.\n */\nexport function setProps(proto, properties, syncToElement = true) {\n for (const prop of properties) {\n Object.defineProperty(proto, prop, {\n configurable: true,\n enumerable: true,\n get() {\n return this._props ? this._props.get(prop) : undefined;\n },\n set(value) {\n if (!this._props) {\n this._props = new Map();\n }\n if (value === this._props.get(prop)) {\n return;\n }\n\n this._props.set(prop, value);\n if (!this.isConnected) {\n return;\n }\n\n const attrValue = getPropValue(typeof value, value, \"toAttribute\");\n syncAttribute(this, prop, attrValue);\n if (syncToElement && this.element) {\n syncAttribute(this.element, prop, attrValue);\n }\n },\n });\n }\n}\n\n/**\n * We need to update the internals of the Elena Element\n * when props on the host element are changed.\n *\n * @param {object} context\n * @param {string} name\n * @param {any} oldValue\n * @param {any} newValue\n */\nexport function getProps(context, name, oldValue, newValue) {\n if (oldValue !== newValue) {\n const type = typeof context[name];\n const newAttr = getPropValue(type, newValue, \"toProp\");\n context[name] = newAttr;\n }\n}\n","/**\n * A base class for Elena Element’s events which\n * defaults to bubbling and composed.\n */\nexport class ElenaEvent extends Event {\n constructor(type, eventInitDict) {\n super(type, {\n bubbles: true,\n composed: true,\n ...eventInitDict,\n });\n }\n}\n","/**\n * Register the Elena Element if the browser supports it.\n *\n * @param {string} tagName\n * @param {Function} Element\n */\nexport function defineElement(tagName, Element) {\n if (typeof window !== \"undefined\" && \"customElements\" in window) {\n if (!window.customElements.get(tagName)) {\n window.customElements.define(tagName, Element);\n }\n }\n}\n\n/**\n * Escape a string for safe insertion into HTML.\n *\n * @param {string} str\n * @returns {string}\n */\nexport function escapeHtml(str) {\n const Escape = { \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\", '\"': \"&quot;\", \"'\": \"&#39;\" };\n return String(str).replace(/[&<>\"']/g, c => Escape[c]);\n}\n\n/**\n * Tagged template for trusted HTML. Use as the return value of render(), or for\n * sub-fragments inside render methods. Interpolated values are auto-escaped;\n * nested `html` fragments are passed through without double-escaping.\n *\n * @param {TemplateStringsArray} strings\n * @param {...*} values\n * @returns {{ __raw: true, strings: TemplateStringsArray, values: Array, toString(): string }}\n */\nexport function html(strings, ...values) {\n const result = strings.reduce((acc, str, i) => {\n const v = values[i];\n return acc + str + (v && v.__raw ? String(v) : escapeHtml(String(v ?? \"\")));\n }, \"\");\n return { __raw: true, strings, values, toString: () => result };\n}\n\n/**\n * A placeholder you can return from a conditional expression inside a template\n * to render nothing. Always produces an empty string; signals to the template\n * engine that no further processing is needed.\n *\n * @type {{ __raw: true, toString(): string }}\n */\nexport const nothing = { __raw: true, toString: () => \"\" };\n","import { escapeHtml } from \"./utils.js\";\n\n/** @type {WeakMap<TemplateStringsArray, string[]>} */\nconst _stringsCache = new WeakMap();\n\n/**\n * Render a tagged template into an Elena Element with DOM diffing.\n *\n * On first render, builds the full HTML markup and render.\n * On re-renders, patches only the text nodes whose values changed,\n * avoiding a full DOM rebuild.\n *\n * Cache state is stored on the element instance:\n * - _tplStrings: reference to the template’s static strings array\n * - _tplValues: array of escaped values from the last render\n * - _tplParts: array mapping each value index to its DOM text node\n *\n * @param {HTMLElement} element\n * @param {TemplateStringsArray} strings - Static parts of the tagged template\n * @param {Array} values - Dynamic interpolated values\n */\nexport function renderTemplate(element, strings, values) {\n if (patchTextNodes(element, strings, values)) {\n return;\n }\n fullRender(element, strings, values);\n}\n\n/**\n * Fast path: patch only the text nodes whose values changed.\n * Returns true if all changes were handled (no full render needed).\n *\n * @param {HTMLElement} element - The host element with cached template state\n * @param {TemplateStringsArray} strings - Static parts of the tagged template\n * @param {Array} values - Dynamic interpolated values\n * @returns {boolean} Whether patching was sufficient (false = full render)\n */\nfunction patchTextNodes(element, strings, values) {\n // Only works when re-rendering the same template shape\n if (element._tplStrings !== strings || !element._tplParts) {\n return false;\n }\n\n let needsFullRender = false;\n\n for (let i = 0; i < values.length; i++) {\n const v = values[i];\n // Check if this value is a trusted HTML fragment (isRaw),\n // reated via the `html` tag, which bypasses escaping\n const isRaw = v && v.__raw;\n const newRendered = isRaw ? String(v) : escapeHtml(String(v ?? \"\"));\n const oldRendered = element._tplValues[i];\n\n if (newRendered === oldRendered) {\n continue;\n }\n\n element._tplValues[i] = newRendered;\n\n // Raw HTML values or attribute-position values require a full render\n if (isRaw) {\n needsFullRender = true;\n } else {\n const textNode = element._tplParts[i];\n if (textNode) {\n // Value is in a text position, update the DOM node directly\n textNode.textContent = String(v ?? \"\");\n } else {\n // Value is in an attribute position, can’t patch, need full render\n needsFullRender = true;\n }\n }\n }\n\n return !needsFullRender;\n}\n\n/**\n * Cold path: build full HTML markup, render it via DocumentFragment,\n * and map each interpolated value to its corresponding DOM text node\n * for future fast-path patching.\n *\n * @param {HTMLElement} element - The host element to render into\n * @param {TemplateStringsArray} strings - Static parts of the tagged template\n * @param {Array} values - Dynamic interpolated values\n */\nfunction fullRender(element, strings, values) {\n const renderedValues = values.map(v => (v && v.__raw ? String(v) : escapeHtml(String(v ?? \"\"))));\n\n // The JS engine reuses the same `strings` object for every call from the same template\n // literal, so cache the cleaned-up parts and run the regex only once per template.\n let processedStrings = _stringsCache.get(strings);\n if (!processedStrings) {\n processedStrings = Array.from(strings, s => s.replace(/\\n\\s*/g, \" \"));\n _stringsCache.set(strings, processedStrings);\n }\n\n // Build the complete HTML markup\n const markup = processedStrings\n .reduce((out, str, i) => out + str + (renderedValues[i] ?? \"\"), \"\")\n .replace(/>\\s+</g, \"><\")\n .replace(/>\\s+/g, \">\")\n .replace(/\\s+</g, \"<\")\n .trim();\n\n renderHtml(element, markup);\n\n // Cache template identity and rendered values\n element._tplStrings = strings;\n element._tplValues = renderedValues;\n\n // Walk text nodes to map each value index to its DOM node\n element._tplParts = mapTextNodes(element, renderedValues);\n}\n\n/**\n * Walk the Elena Element’s text nodes and map each escaped value\n * to its corresponding DOM text node. Values in attribute\n * positions won’t have a matching text node and will be null.\n *\n * Known limitation: text nodes are matched by content. This could\n * cause the wrong node to be patched if a static part of the template\n * contains text that exactly matches a dynamic value with no surrounding\n * tag. In practice this is extremely rare, static template parts should\n * be HTML structure (tags, attributes, punctuation), not raw text.\n *\n * Example: `<span>Elena</span>${name}` with `name = \"Elena\"`. The\n * walker matches the static \"Elena\" inside the span first, so when\n * `name` later changes to \"Bob\", the span text is patched instead\n * of the trailing text node.\n *\n * @param {HTMLElement} element - The host element to walk\n * @param {string[]} escapedValues - HTML-escaped interpolated values\n * @returns {Array<Text | undefined>} Array mapping each value index to its text node\n */\nfunction mapTextNodes(element, escapedValues) {\n const parts = new Array(escapedValues.length);\n const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);\n\n let valueIndex = 0;\n let node;\n\n while ((node = walker.nextNode()) && valueIndex < escapedValues.length) {\n if (node.textContent === escapedValues[valueIndex]) {\n parts[valueIndex] = node;\n valueIndex++;\n }\n }\n\n return parts;\n}\n\n/**\n * Render an HTML string into an element by parsing it into a\n * DocumentFragment via createContextualFragment and swapping\n * the element’s children in a single replaceChildren call.\n *\n * Uses element.ownerDocument.createRange() so that elements\n * in e.g. iframes parse HTML correctly.\n *\n * @param {HTMLElement} element\n * @param {string} markup\n */\nexport function renderHtml(element, markup) {\n if (!element) {\n console.warn(\"░█ [ELENA]: Cannot render to a null element.\");\n return;\n }\n element.replaceChildren(element.ownerDocument.createRange().createContextualFragment(markup));\n}\n","/**\n * ██████████ ████\n * ░░███░░░░░█░░███\n * ░███ █ ░ ░███ ██████ ████████ ██████\n * ░██████ ░███ ███░░███░░███░░███ ░░░░░███\n * ░███░░█ ░███ ░███████ ░███ ░███ ███████\n * ░███ ░ █ ░███ ░███░░░ ░███ ░███ ███░░███\n * ██████████ █████░░██████ ████ █████░░████████\n * ░░░░░░░░░░ ░░░░░ ░░░░░░ ░░░░ ░░░░░ ░░░░░░░░\n *\n * Elena: Progressive Web Components\n * https://elenajs.com\n */\n\nimport { setProps, getProps, getPropValue, syncAttribute } from \"./common/props.js\";\nimport { ElenaEvent } from \"./common/events.js\";\nimport { defineElement, html, nothing } from \"./common/utils.js\";\nimport { renderTemplate } from \"./common/render.js\";\n\nexport { html, nothing };\n\n/**\n * @typedef {Object} ElenaOptions\n * @property {string} [tagName] - Custom element tag name to register (e.g. \"elena-button\").\n * @property {string[]} [props] - Props observed and synced as attributes.\n * @property {string[]} [events] - Events to delegate from the inner element.\n * @property {string} [element] - CSS selector for the inner element.\n */\n\n/**\n * @typedef {new (...args: any[]) => HTMLElement} ElenaConstructor\n */\n\n/**\n * Factory that creates Elena mixin class.\n *\n * Wraps `superClass` with Elena’s lifecycle, templating, props,\n * and events features. The `options` argument is optional.\n *\n * @param {ElenaConstructor} superClass - Base class to extend.\n * @param {ElenaOptions} [options] - Optional configuration options.\n * @returns {CustomElementConstructor} A class ready to be registered.\n */\nexport function Elena(superClass, options) {\n /**\n * Pre-compile element resolver once at class definition\n * time to improve performance:\n *\n * 1. no selector: firstElementChild (property access)\n * 2. className: getElementsByClassName (skips full selector parser)\n * 3. any other: querySelector (full parser, only when needed)\n */\n const resolveElement = !(options && options.element)\n ? host => host.firstElementChild\n : /^[a-z][a-z0-9-]*$/i.test(options.element)\n ? host => host.getElementsByClassName(options.element)[0]\n : host => host.querySelector(options.element);\n\n /**\n * Set up the initial state and default values for Elena Element.\n */\n class ElenaElement extends superClass {\n /**\n * Reference to the base element in the provided template.\n *\n * @type {Object}\n */\n element = null;\n\n /**\n * This method is called when the Elena Element’s\n * props are changed, added, removed or replaced.\n *\n * @param {string} prop\n * @param {string} oldValue\n * @param {string} newValue\n */\n attributeChangedCallback(prop, oldValue, newValue) {\n getProps(this, prop, oldValue, newValue);\n\n // Re-render when attributes change (after initial render).\n // Guard against re-entrant renders: if render() itself mutates an observed\n // attribute, skip the recursive call to prevent an infinite loop.\n // Use _hydrated (set at end of connectedCallback) rather than this.element\n // so Composite Components without an inner element ref also re-render correctly.\n if (this._hydrated && oldValue !== newValue && !this._isRendering) {\n this._isRendering = true;\n this._applyRender();\n this._isRendering = false;\n }\n }\n\n /**\n * This static method returns an array containing the\n * names of the props we want to observe.\n */\n static get observedAttributes() {\n return options && options.props ? options.props : [];\n }\n\n /**\n * Override in a subclass to define the element's HTML structure.\n * Return an `html` tagged template literal.\n * No-op by default: elements without a render method connect safely.\n */\n render() {}\n\n /**\n * @internal\n */\n _applyRender() {\n const result = this.render();\n if (result && result.strings) {\n renderTemplate(this, result.strings, result.values);\n }\n }\n\n /**\n * This method is called each time the Elena Element\n * is added to the document.\n */\n connectedCallback() {\n this._applyRender();\n\n if (!this.element) {\n this.element = resolveElement(this);\n\n if (!this.element) {\n // Only warn when an explicit element selector was provided but didn't match.\n // Composite Components (no element option) intentionally have no inner ref.\n if (options && options.element) {\n console.warn(\"░█ [ELENA]: No element found, using firstElementChild as fallback.\");\n }\n this.element = this.firstElementChild;\n }\n }\n\n // Flush props set before connection to host and inner element attributes\n if (this._props) {\n const hasElementSelector = !!(options && options.element);\n for (const [prop, value] of this._props) {\n const attrValue = getPropValue(typeof value, value, \"toAttribute\");\n syncAttribute(this, prop, attrValue);\n if (hasElementSelector) {\n syncAttribute(this.element, prop, attrValue);\n }\n }\n }\n\n if (!this._events && options && options.events) {\n this._events = true;\n options.events?.forEach(e => {\n this.element.addEventListener(e, this);\n this[e] = (...args) => this.element[e](...args);\n });\n }\n\n this.updated();\n }\n\n /**\n * Perform post-update after each render().\n *\n * @internal\n */\n updated() {\n if (!this._hydrated) {\n this._hydrated = true;\n this.classList.add(\"elena-hydrated\");\n }\n }\n\n /**\n * This method is called each time the Elena Element\n * is removed from the document.\n */\n disconnectedCallback() {\n if (this._events) {\n this._events = false;\n options.events?.forEach(e => {\n this.element?.removeEventListener(e, this);\n });\n }\n }\n\n /**\n * Handles events on the Elena Element.\n *\n * @internal\n */\n handleEvent(event) {\n if (options.events?.includes(event.type)) {\n event.stopPropagation();\n /** @internal */\n this.dispatchEvent(new ElenaEvent(event.type, { cancelable: true }));\n }\n }\n }\n\n if (options && options.props?.length) {\n setProps(ElenaElement.prototype, options.props, !!(options && options.element));\n }\n\n if (options && options.tagName) {\n /** @type {string} */\n ElenaElement._tagName = options.tagName;\n }\n\n /**\n * Register this class as a custom element using the `tagName`\n * set in options. Must be called on the final subclass, not\n * on the Elena mixin directly.\n *\n * @this {CustomElementConstructor}\n */\n ElenaElement.define = function () {\n if (this._tagName) {\n defineElement(this._tagName, this);\n }\n };\n\n return ElenaElement;\n}\n"],"names":["getPropValue","type","value","transform","JSON","stringify","parse","console","warn","syncAttribute","element","name","removeAttribute","setAttribute","ElenaEvent","Event","constructor","eventInitDict","super","bubbles","composed","escapeHtml","str","Escape","String","replace","c","html","strings","values","result","reduce","acc","i","v","__raw","toString","nothing","_stringsCache","WeakMap","renderTemplate","_tplStrings","_tplParts","needsFullRender","length","isRaw","newRendered","_tplValues","textNode","textContent","patchTextNodes","renderedValues","map","processedStrings","get","Array","from","s","set","markup","out","trim","replaceChildren","ownerDocument","createRange","createContextualFragment","renderHtml","escapedValues","parts","walker","document","createTreeWalker","NodeFilter","SHOW_TEXT","node","valueIndex","nextNode","mapTextNodes","fullRender","Elena","superClass","options","resolveElement","test","host","getElementsByClassName","querySelector","firstElementChild","ElenaElement","attributeChangedCallback","prop","oldValue","newValue","context","newAttr","getProps","this","_hydrated","_isRendering","_applyRender","observedAttributes","props","render","connectedCallback","_props","hasElementSelector","attrValue","_events","events","forEach","e","addEventListener","args","updated","classList","add","disconnectedCallback","removeEventListener","handleEvent","event","includes","stopPropagation","dispatchEvent","cancelable","proto","properties","syncToElement","Object","defineProperty","configurable","enumerable","undefined","Map","isConnected","setProps","prototype","tagName","_tagName","define","Element","window","customElements"],"mappings":"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"}
@@ -22,7 +22,7 @@ export function syncAttribute(element: Element, name: string, value: string | nu
22
22
  * @param {Function} proto - The class prototype
23
23
  * @param {string[]} properties - Prop names to define
24
24
  * @param {boolean} [syncToElement=true] - Whether to also sync prop attributes to the
25
- * inner element ref. Pass false for slot components that have no explicit inner
25
+ * inner element ref. Pass false for Composite Components that have no explicit inner
26
26
  * element selector, so props are not leaked onto slotted children.
27
27
  */
28
28
  export function setProps(proto: Function, properties: string[], syncToElement?: boolean): void;
package/dist/elena.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"elena.js","sources":["../src/elena.js"],"sourcesContent":["/**\n * ██████████ ████\n * ░░███░░░░░█░░███\n * ░███ █ ░ ░███ ██████ ████████ ██████\n * ░██████ ░███ ███░░███░░███░░███ ░░░░░███\n * ░███░░█ ░███ ░███████ ░███ ░███ ███████\n * ░███ ░ █ ░███ ░███░░░ ░███ ░███ ███░░███\n * ██████████ █████░░██████ ████ █████░░████████\n * ░░░░░░░░░░ ░░░░░ ░░░░░░ ░░░░ ░░░░░ ░░░░░░░░\n *\n * Elena: Progressive Web Components\n * https://elenajs.com\n */\n\nimport { setProps, getProps, getPropValue, syncAttribute } from \"./common/props.js\";\nimport { ElenaEvent } from \"./common/events.js\";\nimport { defineElement, html, nothing } from \"./common/utils.js\";\nimport { renderTemplate } from \"./common/render.js\";\n\nexport { html, nothing };\n\n/**\n * @typedef {Object} ElenaOptions\n * @property {string} [tagName] - Custom element tag name to register (e.g. \"elena-button\").\n * @property {string[]} [props] - Props observed and synced as attributes.\n * @property {string[]} [events] - Events to delegate from the inner element.\n * @property {string} [element] - CSS selector for the inner element.\n */\n\n/**\n * @typedef {new (...args: any[]) => HTMLElement} ElenaConstructor\n */\n\n/**\n * Factory that creates Elena mixin class.\n *\n * Wraps `superClass` with Elena’s lifecycle, templating, props,\n * and events features. The `options` argument is optional.\n *\n * @param {ElenaConstructor} superClass - Base class to extend.\n * @param {ElenaOptions} [options] - Optional configuration options.\n * @returns {CustomElementConstructor} A class ready to be registered.\n */\nexport function Elena(superClass, options) {\n /**\n * Pre-compile element resolver once at class definition\n * time to improve performance:\n *\n * 1. no selector: firstElementChild (property access)\n * 2. className: getElementsByClassName (skips full selector parser)\n * 3. any other: querySelector (full parser, only when needed)\n */\n const resolveElement = !(options && options.element)\n ? host => host.firstElementChild\n : /^[a-z][a-z0-9-]*$/i.test(options.element)\n ? host => host.getElementsByClassName(options.element)[0]\n : host => host.querySelector(options.element);\n\n /**\n * Set up the initial state and default values for Elena Element.\n */\n class ElenaElement extends superClass {\n /**\n * Reference to the base element in the provided template.\n *\n * @type {Object}\n */\n element = null;\n\n /**\n * This method is called when the Elena Element’s\n * props are changed, added, removed or replaced.\n *\n * @param {string} prop\n * @param {string} oldValue\n * @param {string} newValue\n */\n attributeChangedCallback(prop, oldValue, newValue) {\n getProps(this, prop, oldValue, newValue);\n\n // Re-render when attributes change (after initial render).\n // Guard against re-entrant renders: if render() itself mutates an observed\n // attribute, skip the recursive call to prevent an infinite loop.\n // Use _hydrated (set at end of connectedCallback) rather than this.element\n // so slot components without an inner element ref also re-render correctly.\n if (this._hydrated && oldValue !== newValue && !this._isRendering) {\n this._isRendering = true;\n this._applyRender();\n this._isRendering = false;\n }\n }\n\n /**\n * This static method returns an array containing the\n * names of the props we want to observe.\n */\n static get observedAttributes() {\n return options && options.props ? options.props : [];\n }\n\n /**\n * Override in a subclass to define the element's HTML structure.\n * Return an `html` tagged template literal.\n * No-op by default: elements without a render method connect safely.\n */\n render() {}\n\n /**\n * @internal\n */\n _applyRender() {\n const result = this.render();\n if (result && result.strings) {\n renderTemplate(this, result.strings, result.values);\n }\n }\n\n /**\n * This method is called each time the Elena Element\n * is added to the document.\n */\n connectedCallback() {\n this._applyRender();\n\n if (!this.element) {\n this.element = resolveElement(this);\n\n if (!this.element) {\n // Only warn when an explicit element selector was provided but didn't match.\n // Slot components (no element option) intentionally have no inner ref.\n if (options && options.element) {\n console.warn(\"░█ [ELENA]: No element found, using firstElementChild as fallback.\");\n }\n this.element = this.firstElementChild;\n }\n }\n\n // Flush props set before connection to host and inner element attributes\n if (this._props) {\n const hasElementSelector = !!(options && options.element);\n for (const [prop, value] of this._props) {\n const attrValue = getPropValue(typeof value, value, \"toAttribute\");\n syncAttribute(this, prop, attrValue);\n if (hasElementSelector) {\n syncAttribute(this.element, prop, attrValue);\n }\n }\n }\n\n if (!this._events && options && options.events) {\n this._events = true;\n options.events?.forEach(e => {\n this.element.addEventListener(e, this);\n this[e] = (...args) => this.element[e](...args);\n });\n }\n\n this.updated();\n }\n\n /**\n * Perform post-update after each render().\n *\n * @internal\n */\n updated() {\n if (!this._hydrated) {\n this._hydrated = true;\n this.classList.add(\"elena-hydrated\");\n }\n }\n\n /**\n * This method is called each time the Elena Element\n * is removed from the document.\n */\n disconnectedCallback() {\n if (this._events) {\n this._events = false;\n options.events?.forEach(e => {\n this.element?.removeEventListener(e, this);\n });\n }\n }\n\n /**\n * Handles events on the Elena Element.\n *\n * @internal\n */\n handleEvent(event) {\n if (options.events?.includes(event.type)) {\n event.stopPropagation();\n /** @internal */\n this.dispatchEvent(new ElenaEvent(event.type, { cancelable: true }));\n }\n }\n }\n\n if (options && options.props?.length) {\n setProps(ElenaElement.prototype, options.props, !!(options && options.element));\n }\n\n if (options && options.tagName) {\n /** @type {string} */\n ElenaElement._tagName = options.tagName;\n }\n\n /**\n * Register this class as a custom element using the `tagName`\n * set in options. Must be called on the final subclass, not\n * on the Elena mixin directly.\n *\n * @this {CustomElementConstructor}\n */\n ElenaElement.define = function () {\n if (this._tagName) {\n defineElement(this._tagName, this);\n }\n };\n\n return ElenaElement;\n}\n"],"names":["Elena","superClass","options","resolveElement","element","test","host","getElementsByClassName","querySelector","firstElementChild","ElenaElement","attributeChangedCallback","prop","oldValue","newValue","getProps","this","_hydrated","_isRendering","_applyRender","observedAttributes","props","render","result","strings","renderTemplate","values","connectedCallback","console","warn","_props","hasElementSelector","value","attrValue","getPropValue","syncAttribute","_events","events","forEach","e","addEventListener","args","updated","classList","add","disconnectedCallback","removeEventListener","handleEvent","event","includes","type","stopPropagation","dispatchEvent","ElenaEvent","cancelable","length","setProps","prototype","tagName","_tagName","define","defineElement"],"mappings":"+PA2CO,SAASA,EAAMC,EAAYC,GAShC,MAAMC,EAAmBD,GAAWA,EAAQE,QAExC,qBAAqBC,KAAKH,EAAQE,SAChCE,GAAQA,EAAKC,uBAAuBL,EAAQE,SAAS,GACrDE,GAAQA,EAAKE,cAAcN,EAAQE,SAHrCE,GAAQA,EAAKG,kBAQjB,MAAMC,UAAqBT,EAMzBG,QAAU,KAUV,wBAAAO,CAAyBC,EAAMC,EAAUC,GACvCC,EAASC,KAAMJ,EAAMC,EAAUC,GAO3BE,KAAKC,WAAaJ,IAAaC,IAAaE,KAAKE,eACnDF,KAAKE,cAAe,EACpBF,KAAKG,eACLH,KAAKE,cAAe,EAExB,CAMA,6BAAWE,GACT,OAAOlB,GAAWA,EAAQmB,MAAQnB,EAAQmB,MAAQ,EACpD,CAOA,MAAAC,GAAU,CAKV,YAAAH,GACE,MAAMI,EAASP,KAAKM,SAChBC,GAAUA,EAAOC,SACnBC,EAAeT,KAAMO,EAAOC,QAASD,EAAOG,OAEhD,CAMA,iBAAAC,GAiBE,GAhBAX,KAAKG,eAEAH,KAAKZ,UACRY,KAAKZ,QAAUD,EAAea,MAEzBA,KAAKZ,UAGJF,GAAWA,EAAQE,SACrBwB,QAAQC,KAAK,sEAEfb,KAAKZ,QAAUY,KAAKP,oBAKpBO,KAAKc,OAAQ,CACf,MAAMC,KAAwB7B,IAAWA,EAAQE,SACjD,IAAK,MAAOQ,EAAMoB,KAAUhB,KAAKc,OAAQ,CACvC,MAAMG,EAAYC,SAAoBF,EAAOA,EAAO,eACpDG,EAAcnB,KAAMJ,EAAMqB,GACtBF,GACFI,EAAcnB,KAAKZ,QAASQ,EAAMqB,EAEtC,CACF,EAEKjB,KAAKoB,SAAWlC,GAAWA,EAAQmC,SACtCrB,KAAKoB,SAAU,EACflC,EAAQmC,QAAQC,QAAQC,IACtBvB,KAAKZ,QAAQoC,iBAAiBD,EAAGvB,MACjCA,KAAKuB,GAAK,IAAIE,IAASzB,KAAKZ,QAAQmC,MAAME,MAI9CzB,KAAK0B,SACP,CAOA,OAAAA,GACO1B,KAAKC,YACRD,KAAKC,WAAY,EACjBD,KAAK2B,UAAUC,IAAI,kBAEvB,CAMA,oBAAAC,GACM7B,KAAKoB,UACPpB,KAAKoB,SAAU,EACflC,EAAQmC,QAAQC,QAAQC,IACtBvB,KAAKZ,SAAS0C,oBAAoBP,EAAGvB,QAG3C,CAOA,WAAA+B,CAAYC,GACN9C,EAAQmC,QAAQY,SAASD,EAAME,QACjCF,EAAMG,kBAENnC,KAAKoC,cAAc,IAAIC,EAAWL,EAAME,KAAM,CAAEI,YAAY,KAEhE,EAyBF,OAtBIpD,GAAWA,EAAQmB,OAAOkC,QAC5BC,EAAS9C,EAAa+C,UAAWvD,EAAQmB,SAAUnB,IAAWA,EAAQE,UAGpEF,GAAWA,EAAQwD,UAErBhD,EAAaiD,SAAWzD,EAAQwD,SAUlChD,EAAakD,OAAS,WAChB5C,KAAK2C,UACPE,EAAc7C,KAAK2C,SAAU3C,KAEjC,EAEON,CACT"}
1
+ {"version":3,"file":"elena.js","sources":["../src/elena.js"],"sourcesContent":["/**\n * ██████████ ████\n * ░░███░░░░░█░░███\n * ░███ █ ░ ░███ ██████ ████████ ██████\n * ░██████ ░███ ███░░███░░███░░███ ░░░░░███\n * ░███░░█ ░███ ░███████ ░███ ░███ ███████\n * ░███ ░ █ ░███ ░███░░░ ░███ ░███ ███░░███\n * ██████████ █████░░██████ ████ █████░░████████\n * ░░░░░░░░░░ ░░░░░ ░░░░░░ ░░░░ ░░░░░ ░░░░░░░░\n *\n * Elena: Progressive Web Components\n * https://elenajs.com\n */\n\nimport { setProps, getProps, getPropValue, syncAttribute } from \"./common/props.js\";\nimport { ElenaEvent } from \"./common/events.js\";\nimport { defineElement, html, nothing } from \"./common/utils.js\";\nimport { renderTemplate } from \"./common/render.js\";\n\nexport { html, nothing };\n\n/**\n * @typedef {Object} ElenaOptions\n * @property {string} [tagName] - Custom element tag name to register (e.g. \"elena-button\").\n * @property {string[]} [props] - Props observed and synced as attributes.\n * @property {string[]} [events] - Events to delegate from the inner element.\n * @property {string} [element] - CSS selector for the inner element.\n */\n\n/**\n * @typedef {new (...args: any[]) => HTMLElement} ElenaConstructor\n */\n\n/**\n * Factory that creates Elena mixin class.\n *\n * Wraps `superClass` with Elena’s lifecycle, templating, props,\n * and events features. The `options` argument is optional.\n *\n * @param {ElenaConstructor} superClass - Base class to extend.\n * @param {ElenaOptions} [options] - Optional configuration options.\n * @returns {CustomElementConstructor} A class ready to be registered.\n */\nexport function Elena(superClass, options) {\n /**\n * Pre-compile element resolver once at class definition\n * time to improve performance:\n *\n * 1. no selector: firstElementChild (property access)\n * 2. className: getElementsByClassName (skips full selector parser)\n * 3. any other: querySelector (full parser, only when needed)\n */\n const resolveElement = !(options && options.element)\n ? host => host.firstElementChild\n : /^[a-z][a-z0-9-]*$/i.test(options.element)\n ? host => host.getElementsByClassName(options.element)[0]\n : host => host.querySelector(options.element);\n\n /**\n * Set up the initial state and default values for Elena Element.\n */\n class ElenaElement extends superClass {\n /**\n * Reference to the base element in the provided template.\n *\n * @type {Object}\n */\n element = null;\n\n /**\n * This method is called when the Elena Element’s\n * props are changed, added, removed or replaced.\n *\n * @param {string} prop\n * @param {string} oldValue\n * @param {string} newValue\n */\n attributeChangedCallback(prop, oldValue, newValue) {\n getProps(this, prop, oldValue, newValue);\n\n // Re-render when attributes change (after initial render).\n // Guard against re-entrant renders: if render() itself mutates an observed\n // attribute, skip the recursive call to prevent an infinite loop.\n // Use _hydrated (set at end of connectedCallback) rather than this.element\n // so Composite Components without an inner element ref also re-render correctly.\n if (this._hydrated && oldValue !== newValue && !this._isRendering) {\n this._isRendering = true;\n this._applyRender();\n this._isRendering = false;\n }\n }\n\n /**\n * This static method returns an array containing the\n * names of the props we want to observe.\n */\n static get observedAttributes() {\n return options && options.props ? options.props : [];\n }\n\n /**\n * Override in a subclass to define the element's HTML structure.\n * Return an `html` tagged template literal.\n * No-op by default: elements without a render method connect safely.\n */\n render() {}\n\n /**\n * @internal\n */\n _applyRender() {\n const result = this.render();\n if (result && result.strings) {\n renderTemplate(this, result.strings, result.values);\n }\n }\n\n /**\n * This method is called each time the Elena Element\n * is added to the document.\n */\n connectedCallback() {\n this._applyRender();\n\n if (!this.element) {\n this.element = resolveElement(this);\n\n if (!this.element) {\n // Only warn when an explicit element selector was provided but didn't match.\n // Composite Components (no element option) intentionally have no inner ref.\n if (options && options.element) {\n console.warn(\"░█ [ELENA]: No element found, using firstElementChild as fallback.\");\n }\n this.element = this.firstElementChild;\n }\n }\n\n // Flush props set before connection to host and inner element attributes\n if (this._props) {\n const hasElementSelector = !!(options && options.element);\n for (const [prop, value] of this._props) {\n const attrValue = getPropValue(typeof value, value, \"toAttribute\");\n syncAttribute(this, prop, attrValue);\n if (hasElementSelector) {\n syncAttribute(this.element, prop, attrValue);\n }\n }\n }\n\n if (!this._events && options && options.events) {\n this._events = true;\n options.events?.forEach(e => {\n this.element.addEventListener(e, this);\n this[e] = (...args) => this.element[e](...args);\n });\n }\n\n this.updated();\n }\n\n /**\n * Perform post-update after each render().\n *\n * @internal\n */\n updated() {\n if (!this._hydrated) {\n this._hydrated = true;\n this.classList.add(\"elena-hydrated\");\n }\n }\n\n /**\n * This method is called each time the Elena Element\n * is removed from the document.\n */\n disconnectedCallback() {\n if (this._events) {\n this._events = false;\n options.events?.forEach(e => {\n this.element?.removeEventListener(e, this);\n });\n }\n }\n\n /**\n * Handles events on the Elena Element.\n *\n * @internal\n */\n handleEvent(event) {\n if (options.events?.includes(event.type)) {\n event.stopPropagation();\n /** @internal */\n this.dispatchEvent(new ElenaEvent(event.type, { cancelable: true }));\n }\n }\n }\n\n if (options && options.props?.length) {\n setProps(ElenaElement.prototype, options.props, !!(options && options.element));\n }\n\n if (options && options.tagName) {\n /** @type {string} */\n ElenaElement._tagName = options.tagName;\n }\n\n /**\n * Register this class as a custom element using the `tagName`\n * set in options. Must be called on the final subclass, not\n * on the Elena mixin directly.\n *\n * @this {CustomElementConstructor}\n */\n ElenaElement.define = function () {\n if (this._tagName) {\n defineElement(this._tagName, this);\n }\n };\n\n return ElenaElement;\n}\n"],"names":["Elena","superClass","options","resolveElement","element","test","host","getElementsByClassName","querySelector","firstElementChild","ElenaElement","attributeChangedCallback","prop","oldValue","newValue","getProps","this","_hydrated","_isRendering","_applyRender","observedAttributes","props","render","result","strings","renderTemplate","values","connectedCallback","console","warn","_props","hasElementSelector","value","attrValue","getPropValue","syncAttribute","_events","events","forEach","e","addEventListener","args","updated","classList","add","disconnectedCallback","removeEventListener","handleEvent","event","includes","type","stopPropagation","dispatchEvent","ElenaEvent","cancelable","length","setProps","prototype","tagName","_tagName","define","defineElement"],"mappings":"+PA2CO,SAASA,EAAMC,EAAYC,GAShC,MAAMC,EAAmBD,GAAWA,EAAQE,QAExC,qBAAqBC,KAAKH,EAAQE,SAChCE,GAAQA,EAAKC,uBAAuBL,EAAQE,SAAS,GACrDE,GAAQA,EAAKE,cAAcN,EAAQE,SAHrCE,GAAQA,EAAKG,kBAQjB,MAAMC,UAAqBT,EAMzBG,QAAU,KAUV,wBAAAO,CAAyBC,EAAMC,EAAUC,GACvCC,EAASC,KAAMJ,EAAMC,EAAUC,GAO3BE,KAAKC,WAAaJ,IAAaC,IAAaE,KAAKE,eACnDF,KAAKE,cAAe,EACpBF,KAAKG,eACLH,KAAKE,cAAe,EAExB,CAMA,6BAAWE,GACT,OAAOlB,GAAWA,EAAQmB,MAAQnB,EAAQmB,MAAQ,EACpD,CAOA,MAAAC,GAAU,CAKV,YAAAH,GACE,MAAMI,EAASP,KAAKM,SAChBC,GAAUA,EAAOC,SACnBC,EAAeT,KAAMO,EAAOC,QAASD,EAAOG,OAEhD,CAMA,iBAAAC,GAiBE,GAhBAX,KAAKG,eAEAH,KAAKZ,UACRY,KAAKZ,QAAUD,EAAea,MAEzBA,KAAKZ,UAGJF,GAAWA,EAAQE,SACrBwB,QAAQC,KAAK,sEAEfb,KAAKZ,QAAUY,KAAKP,oBAKpBO,KAAKc,OAAQ,CACf,MAAMC,KAAwB7B,IAAWA,EAAQE,SACjD,IAAK,MAAOQ,EAAMoB,KAAUhB,KAAKc,OAAQ,CACvC,MAAMG,EAAYC,SAAoBF,EAAOA,EAAO,eACpDG,EAAcnB,KAAMJ,EAAMqB,GACtBF,GACFI,EAAcnB,KAAKZ,QAASQ,EAAMqB,EAEtC,CACF,EAEKjB,KAAKoB,SAAWlC,GAAWA,EAAQmC,SACtCrB,KAAKoB,SAAU,EACflC,EAAQmC,QAAQC,QAAQC,IACtBvB,KAAKZ,QAAQoC,iBAAiBD,EAAGvB,MACjCA,KAAKuB,GAAK,IAAIE,IAASzB,KAAKZ,QAAQmC,MAAME,MAI9CzB,KAAK0B,SACP,CAOA,OAAAA,GACO1B,KAAKC,YACRD,KAAKC,WAAY,EACjBD,KAAK2B,UAAUC,IAAI,kBAEvB,CAMA,oBAAAC,GACM7B,KAAKoB,UACPpB,KAAKoB,SAAU,EACflC,EAAQmC,QAAQC,QAAQC,IACtBvB,KAAKZ,SAAS0C,oBAAoBP,EAAGvB,QAG3C,CAOA,WAAA+B,CAAYC,GACN9C,EAAQmC,QAAQY,SAASD,EAAME,QACjCF,EAAMG,kBAENnC,KAAKoC,cAAc,IAAIC,EAAWL,EAAME,KAAM,CAAEI,YAAY,KAEhE,EAyBF,OAtBIpD,GAAWA,EAAQmB,OAAOkC,QAC5BC,EAAS9C,EAAa+C,UAAWvD,EAAQmB,SAAUnB,IAAWA,EAAQE,UAGpEF,GAAWA,EAAQwD,UAErBhD,EAAaiD,SAAWzD,EAAQwD,SAUlChD,EAAakD,OAAS,WAChB5C,KAAK2C,UACPE,EAAc7C,KAAK2C,SAAU3C,KAEjC,EAEON,CACT"}
package/dist/props.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"props.js","sources":["../src/common/props.js"],"sourcesContent":["/**\n * Get the value of the Elena Element property.\n *\n * @param {string} type\n * @param {any} value\n * @param {\"toAttribute\" | \"toProp\"} [transform]\n */\nexport function getPropValue(type, value, transform) {\n value = type === \"boolean\" && typeof value !== \"boolean\" ? value !== null : value;\n\n if (!transform) {\n return value;\n } else if (transform === \"toAttribute\") {\n switch (type) {\n case \"object\":\n case \"array\":\n return value === null ? null : JSON.stringify(value);\n case \"boolean\":\n return value ? \"\" : null;\n case \"number\":\n return value === null ? null : value;\n default:\n return value === \"\" ? null : value;\n }\n } else {\n switch (type) {\n case \"object\":\n case \"array\":\n if (!value) {\n return value;\n }\n try {\n return JSON.parse(value);\n } catch {\n console.warn(\"░█ [ELENA]: Invalid JSON for prop, received: \" + value);\n return null;\n }\n case \"boolean\":\n return value; // conversion already handled above\n case \"number\":\n return value !== null ? +value : value;\n default:\n return value;\n }\n }\n}\n\n/**\n * Set or remove an attribute on an Elena Element.\n *\n * @param {Element} element - Target element\n * @param {string} name - Attribute name\n * @param {string | null} value - Attribute value, or null to remove\n */\nexport function syncAttribute(element, name, value) {\n if (!element) {\n console.warn(\"░█ [ELENA]: Cannot sync attributes to a null element.\");\n return;\n }\n if (value === null) {\n element.removeAttribute(name);\n } else {\n element.setAttribute(name, value);\n }\n}\n\n/**\n * Define prop getters/setters on the prototype once\n * at class-creation time. Values are stored per-instance\n * via a `_props` Map that is lazily created.\n *\n * @param {Function} proto - The class prototype\n * @param {string[]} properties - Prop names to define\n * @param {boolean} [syncToElement=true] - Whether to also sync prop attributes to the\n * inner element ref. Pass false for slot components that have no explicit inner\n * element selector, so props are not leaked onto slotted children.\n */\nexport function setProps(proto, properties, syncToElement = true) {\n for (const prop of properties) {\n Object.defineProperty(proto, prop, {\n configurable: true,\n enumerable: true,\n get() {\n return this._props ? this._props.get(prop) : undefined;\n },\n set(value) {\n if (!this._props) {\n this._props = new Map();\n }\n if (value === this._props.get(prop)) {\n return;\n }\n\n this._props.set(prop, value);\n if (!this.isConnected) {\n return;\n }\n\n const attrValue = getPropValue(typeof value, value, \"toAttribute\");\n syncAttribute(this, prop, attrValue);\n if (syncToElement && this.element) {\n syncAttribute(this.element, prop, attrValue);\n }\n },\n });\n }\n}\n\n/**\n * We need to update the internals of the Elena Element\n * when props on the host element are changed.\n *\n * @param {object} context\n * @param {string} name\n * @param {any} oldValue\n * @param {any} newValue\n */\nexport function getProps(context, name, oldValue, newValue) {\n if (oldValue !== newValue) {\n const type = typeof context[name];\n const newAttr = getPropValue(type, newValue, \"toProp\");\n context[name] = newAttr;\n }\n}\n"],"names":["getPropValue","type","value","transform","JSON","stringify","parse","console","warn","syncAttribute","element","name","removeAttribute","setAttribute","setProps","proto","properties","syncToElement","prop","Object","defineProperty","configurable","enumerable","get","this","_props","undefined","set","Map","isConnected","attrValue","getProps","context","oldValue","newValue","newAttr"],"mappings":"AAOO,SAASA,EAAaC,EAAMC,EAAOC,GAGxC,GAFAD,EAAiB,YAATD,GAAuC,kBAAVC,EAAgC,OAAVA,EAAiBA,GAEvEC,EACH,OAAOD,EACF,GAAkB,gBAAdC,EACT,OAAQF,GACN,IAAK,SACL,IAAK,QACH,OAAiB,OAAVC,EAAiB,KAAOE,KAAKC,UAAUH,GAChD,IAAK,UACH,OAAOA,EAAQ,GAAK,KACtB,IAAK,SACH,OAAiB,OAAVA,EAAiB,KAAOA,EACjC,QACE,MAAiB,KAAVA,EAAe,KAAOA,OAGjC,OAAQD,GACN,IAAK,SACL,IAAK,QACH,IAAKC,EACH,OAAOA,EAET,IACE,OAAOE,KAAKE,MAAMJ,EACpB,CAAE,MAEA,OADAK,QAAQC,KAAK,gDAAkDN,GACxD,IACT,CACF,IAAK,UACH,OAAOA,EACT,IAAK,SACH,OAAiB,OAAVA,GAAkBA,EAAQA,EACnC,QACE,OAAOA,EAGf,CASO,SAASO,EAAcC,EAASC,EAAMT,GACtCQ,EAIS,OAAVR,EACFQ,EAAQE,gBAAgBD,GAExBD,EAAQG,aAAaF,EAAMT,GAN3BK,QAAQC,KAAK,wDAQjB,CAaO,SAASM,EAASC,EAAOC,EAAYC,GAAgB,GAC1D,IAAK,MAAMC,KAAQF,EACjBG,OAAOC,eAAeL,EAAOG,EAAM,CACjCG,cAAc,EACdC,YAAY,EACZ,GAAAC,GACE,OAAOC,KAAKC,OAASD,KAAKC,OAAOF,IAAIL,QAAQQ,CAC/C,EACA,GAAAC,CAAIzB,GAIF,GAHKsB,KAAKC,SACRD,KAAKC,OAAS,IAAIG,KAEhB1B,IAAUsB,KAAKC,OAAOF,IAAIL,GAC5B,OAIF,GADAM,KAAKC,OAAOE,IAAIT,EAAMhB,IACjBsB,KAAKK,YACR,OAGF,MAAMC,EAAY9B,SAAoBE,EAAOA,EAAO,eACpDO,EAAce,KAAMN,EAAMY,GACtBb,GAAiBO,KAAKd,SACxBD,EAAce,KAAKd,QAASQ,EAAMY,EAEtC,GAGN,CAWO,SAASC,EAASC,EAASrB,EAAMsB,EAAUC,GAChD,GAAID,IAAaC,EAAU,CACzB,MACMC,EAAUnC,SADIgC,EAAQrB,GACOuB,EAAU,UAC7CF,EAAQrB,GAAQwB,CAClB,CACF"}
1
+ {"version":3,"file":"props.js","sources":["../src/common/props.js"],"sourcesContent":["/**\n * Get the value of the Elena Element property.\n *\n * @param {string} type\n * @param {any} value\n * @param {\"toAttribute\" | \"toProp\"} [transform]\n */\nexport function getPropValue(type, value, transform) {\n value = type === \"boolean\" && typeof value !== \"boolean\" ? value !== null : value;\n\n if (!transform) {\n return value;\n } else if (transform === \"toAttribute\") {\n switch (type) {\n case \"object\":\n case \"array\":\n return value === null ? null : JSON.stringify(value);\n case \"boolean\":\n return value ? \"\" : null;\n case \"number\":\n return value === null ? null : value;\n default:\n return value === \"\" ? null : value;\n }\n } else {\n switch (type) {\n case \"object\":\n case \"array\":\n if (!value) {\n return value;\n }\n try {\n return JSON.parse(value);\n } catch {\n console.warn(\"░█ [ELENA]: Invalid JSON for prop, received: \" + value);\n return null;\n }\n case \"boolean\":\n return value; // conversion already handled above\n case \"number\":\n return value !== null ? +value : value;\n default:\n return value;\n }\n }\n}\n\n/**\n * Set or remove an attribute on an Elena Element.\n *\n * @param {Element} element - Target element\n * @param {string} name - Attribute name\n * @param {string | null} value - Attribute value, or null to remove\n */\nexport function syncAttribute(element, name, value) {\n if (!element) {\n console.warn(\"░█ [ELENA]: Cannot sync attributes to a null element.\");\n return;\n }\n if (value === null) {\n element.removeAttribute(name);\n } else {\n element.setAttribute(name, value);\n }\n}\n\n/**\n * Define prop getters/setters on the prototype once\n * at class-creation time. Values are stored per-instance\n * via a `_props` Map that is lazily created.\n *\n * @param {Function} proto - The class prototype\n * @param {string[]} properties - Prop names to define\n * @param {boolean} [syncToElement=true] - Whether to also sync prop attributes to the\n * inner element ref. Pass false for Composite Components that have no explicit inner\n * element selector, so props are not leaked onto slotted children.\n */\nexport function setProps(proto, properties, syncToElement = true) {\n for (const prop of properties) {\n Object.defineProperty(proto, prop, {\n configurable: true,\n enumerable: true,\n get() {\n return this._props ? this._props.get(prop) : undefined;\n },\n set(value) {\n if (!this._props) {\n this._props = new Map();\n }\n if (value === this._props.get(prop)) {\n return;\n }\n\n this._props.set(prop, value);\n if (!this.isConnected) {\n return;\n }\n\n const attrValue = getPropValue(typeof value, value, \"toAttribute\");\n syncAttribute(this, prop, attrValue);\n if (syncToElement && this.element) {\n syncAttribute(this.element, prop, attrValue);\n }\n },\n });\n }\n}\n\n/**\n * We need to update the internals of the Elena Element\n * when props on the host element are changed.\n *\n * @param {object} context\n * @param {string} name\n * @param {any} oldValue\n * @param {any} newValue\n */\nexport function getProps(context, name, oldValue, newValue) {\n if (oldValue !== newValue) {\n const type = typeof context[name];\n const newAttr = getPropValue(type, newValue, \"toProp\");\n context[name] = newAttr;\n }\n}\n"],"names":["getPropValue","type","value","transform","JSON","stringify","parse","console","warn","syncAttribute","element","name","removeAttribute","setAttribute","setProps","proto","properties","syncToElement","prop","Object","defineProperty","configurable","enumerable","get","this","_props","undefined","set","Map","isConnected","attrValue","getProps","context","oldValue","newValue","newAttr"],"mappings":"AAOO,SAASA,EAAaC,EAAMC,EAAOC,GAGxC,GAFAD,EAAiB,YAATD,GAAuC,kBAAVC,EAAgC,OAAVA,EAAiBA,GAEvEC,EACH,OAAOD,EACF,GAAkB,gBAAdC,EACT,OAAQF,GACN,IAAK,SACL,IAAK,QACH,OAAiB,OAAVC,EAAiB,KAAOE,KAAKC,UAAUH,GAChD,IAAK,UACH,OAAOA,EAAQ,GAAK,KACtB,IAAK,SACH,OAAiB,OAAVA,EAAiB,KAAOA,EACjC,QACE,MAAiB,KAAVA,EAAe,KAAOA,OAGjC,OAAQD,GACN,IAAK,SACL,IAAK,QACH,IAAKC,EACH,OAAOA,EAET,IACE,OAAOE,KAAKE,MAAMJ,EACpB,CAAE,MAEA,OADAK,QAAQC,KAAK,gDAAkDN,GACxD,IACT,CACF,IAAK,UACH,OAAOA,EACT,IAAK,SACH,OAAiB,OAAVA,GAAkBA,EAAQA,EACnC,QACE,OAAOA,EAGf,CASO,SAASO,EAAcC,EAASC,EAAMT,GACtCQ,EAIS,OAAVR,EACFQ,EAAQE,gBAAgBD,GAExBD,EAAQG,aAAaF,EAAMT,GAN3BK,QAAQC,KAAK,wDAQjB,CAaO,SAASM,EAASC,EAAOC,EAAYC,GAAgB,GAC1D,IAAK,MAAMC,KAAQF,EACjBG,OAAOC,eAAeL,EAAOG,EAAM,CACjCG,cAAc,EACdC,YAAY,EACZ,GAAAC,GACE,OAAOC,KAAKC,OAASD,KAAKC,OAAOF,IAAIL,QAAQQ,CAC/C,EACA,GAAAC,CAAIzB,GAIF,GAHKsB,KAAKC,SACRD,KAAKC,OAAS,IAAIG,KAEhB1B,IAAUsB,KAAKC,OAAOF,IAAIL,GAC5B,OAIF,GADAM,KAAKC,OAAOE,IAAIT,EAAMhB,IACjBsB,KAAKK,YACR,OAGF,MAAMC,EAAY9B,SAAoBE,EAAOA,EAAO,eACpDO,EAAce,KAAMN,EAAMY,GACtBb,GAAiBO,KAAKd,SACxBD,EAAce,KAAKd,QAASQ,EAAMY,EAEtC,GAGN,CAWO,SAASC,EAASC,EAASrB,EAAMsB,EAAUC,GAChD,GAAID,IAAaC,EAAU,CACzB,MACMC,EAAUnC,SADIgC,EAAQrB,GACOuB,EAAU,UAC7CF,EAAQrB,GAAQwB,CAClB,CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elenajs/core",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Elena is a lightweight, opinionated library for building progressive web components.",
5
5
  "author": "Elena <hi@elenajs.com>",
6
6
  "homepage": "https://elenajs.com/",
@@ -20,17 +20,21 @@
20
20
  "build": "NODE_OPTIONS='--no-warnings' rollup -c",
21
21
  "postbuild": "npx -p typescript tsc",
22
22
  "test": "vitest run --coverage",
23
+ "test:visual": "npx playwright test",
24
+ "test:visual:update": "npx playwright test --update-snapshots",
23
25
  "bench": "vitest bench",
24
26
  "clean": "rm -rf dist/"
25
27
  },
26
28
  "devDependencies": {
29
+ "@playwright/test": "^1.58.2",
27
30
  "@rollup/plugin-terser": "0.4.4",
28
31
  "@vitest/coverage-v8": "4.0.18",
29
32
  "happy-dom": "20.5.0",
30
33
  "rollup": "4.57.1",
31
34
  "rollup-plugin-summary": "3.0.1",
35
+ "serve": "^14.2.5",
32
36
  "typescript": "5.9.3",
33
37
  "vitest": "4.0.18"
34
38
  },
35
- "gitHead": "26b5fcaa2941ef54df4f3575571b5528b6dc092a"
39
+ "gitHead": "07f778059a88d5afa61e5917eea501f72bce20f5"
36
40
  }