@gluon/gluon 2.5.2 → 3.0.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.
@@ -0,0 +1,124 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2026 Goffert van Gool
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { type PropertyDeclarations, type PropertyValues } from "./properties.js";
7
+ import { type Styles } from "./styles.js";
8
+ type GluonConstructor = typeof GluonElement;
9
+ /**
10
+ * Registers an element class with a custom element registry, under the tag name `name`
11
+ *
12
+ * A name that is already registered is not registered again, so a module can be loaded more than once.
13
+ * When the name is registered with a different class, the existing registration is kept and a warning is logged.
14
+ */
15
+ export declare const define: <T extends GluonConstructor>(ctor: T, name: string, registry?: CustomElementRegistry) => T;
16
+ /**
17
+ * A base class for custom elements
18
+ *
19
+ * - Declares reactive properties in a static `properties` map, with attributes that are converted and reflected
20
+ * - Renders the result of `render()` with lite-html into a shadow root, batched in a microtask after properties change
21
+ * - Applies the static `styles` to the shadow root, once per class
22
+ * - Calls event handlers in the template with the element as `this`
23
+ */
24
+ export declare class GluonElement extends HTMLElement {
25
+ #private;
26
+ /**
27
+
28
+ * The reactive properties of this element, by name
29
+ *
30
+ * Each declared property is an accessor that requests an update when its value changes.
31
+ * With `attribute: true`, the property is also set from the attribute with its kebab-cased name.
32
+ */
33
+ static properties: PropertyDeclarations;
34
+ /**
35
+ * The styles of this element: CSS text, a constructed `CSSStyleSheet`, or a list of them
36
+ *
37
+ * The styles are applied to the shadow root of every instance, once per class.
38
+ * They are not applied when the element renders into its light DOM.
39
+ */
40
+ static styles?: Styles;
41
+ /**
42
+ * The options for the shadow root that `createRenderRoot()` attaches
43
+ */
44
+ static shadowRootOptions: ShadowRootInit;
45
+ static get observedAttributes(): string[];
46
+ /**
47
+ * The node that `render()` renders into. A shadow root, unless `createRenderRoot()` is overridden
48
+ */
49
+ readonly renderRoot: ParentNode;
50
+ /**
51
+ * Whether an update is scheduled and has not been performed yet
52
+ */
53
+ isUpdatePending: boolean;
54
+ /**
55
+ * Whether the element has performed its first update
56
+ */
57
+ hasUpdated: boolean;
58
+ constructor();
59
+ /**
60
+ * Returns the node to render into
61
+ *
62
+ * Attaches a shadow root with the static `shadowRootOptions`, or returns the shadow root that already exists.
63
+ * Override this method to render somewhere else, for example `return this` to render into the light DOM.
64
+ */
65
+ protected createRenderRoot(): ParentNode;
66
+ /**
67
+ * An `AbortSignal` that is aborted when the element is disconnected
68
+ *
69
+ * Pass it as the `signal` of listeners and observers that are set up in `connectedCallback()`,
70
+ * so they are removed when the element is disconnected. A new signal is created on every connect.
71
+ */
72
+ get disconnectSignal(): AbortSignal;
73
+ /**
74
+ * Resolves when the pending update completes
75
+ *
76
+ * Resolves with `true` when no further update was requested during the update, and `false` otherwise
77
+ */
78
+ get updateComplete(): Promise<boolean>;
79
+ connectedCallback(): void;
80
+ disconnectedCallback(): void;
81
+ attributeChangedCallback(name: string, _oldValue: string | null, value: string | null): void;
82
+ /**
83
+ * Requests an update, which is performed in a microtask
84
+ *
85
+ * Called by the setter of every declared property. Call it without arguments to update after a change
86
+ * that the element cannot see, such as a mutation of an array or object in a property.
87
+ */
88
+ requestUpdate(name?: string, oldValue?: unknown): void;
89
+ /**
90
+ * Performs the pending update now, instead of in a microtask
91
+ *
92
+ * Runs `shouldUpdate()`, `willUpdate()`, `update()`, `firstUpdated()`, and `updated()`
93
+ */
94
+ performUpdate(): void;
95
+ /**
96
+ * Decides whether an update is performed. Defaults to `true`
97
+ */
98
+ protected shouldUpdate(_changed: PropertyValues<this>): boolean;
99
+ /**
100
+ * Called before `update()`, to compute values that depend on the changed properties
101
+ */
102
+ protected willUpdate(_changed: PropertyValues<this>): void;
103
+ /**
104
+ * Reflects the changed properties to their attributes, and renders the result of `render()` into the render root
105
+ *
106
+ * Nothing is rendered when `render()` returns `noChange`
107
+ */
108
+ protected update(_changed: PropertyValues<this>): void;
109
+ /**
110
+ * Returns what to render into the render root, usually a lite-html template
111
+ *
112
+ * Return `noChange` to leave the render root as it is
113
+ */
114
+ protected render(): unknown;
115
+ /**
116
+ * Called after the first update
117
+ */
118
+ protected firstUpdated(_changed: PropertyValues<this>): void;
119
+ /**
120
+ * Called after every update
121
+ */
122
+ protected updated(_changed: PropertyValues<this>): void;
123
+ }
124
+ export {};
@@ -0,0 +1,4 @@
1
+ /** @license Copyright (c) 2026 Goffert van Gool, SPDX-License-Identifier: MIT */
2
+ var e=Math.random().toString(36).slice(2).padStart(10,`0`),t=`attribute-${e}`,n=`comment-${e}`,r=`node-${e}`,i=`${r}" ${r} `,a=()=>document.createComment(``),o=(e,t,n)=>{let r=e.nextSibling;for(;r&&r!==t;){let e=r.nextSibling;n?n.append(r):r.remove(),r=e}},s=/* @__PURE__ */ new WeakSet,c=e=>typeof e==`function`&&s.has(e),l=e=>(s.add(e),e),u=e=>typeof e==`string`||typeof e==`number`||typeof e==`boolean`,d=e=>typeof e[Symbol.iterator]==`function`,ee=e=>typeof e.then==`function`,f={},p={},m={},h=class e{node;value;beforeNode;afterNode;instance;iterableParts;promise;options;constructor({node:e,options:t}){this.node=p,this.value=f,this.options=t,this.beforeNode=e,this.afterNode=e.nextSibling}get parentNode(){return this.beforeNode.parentNode}render(e){if(c(e))e(this);else if(e!==f){if(e==null)this.clear();else if(u(e))this._renderText(e);else if(e instanceof D)this._renderTemplateResult(e);else if(d(e))this._renderIterable(e);else if(e instanceof Node)this._renderNode(e);else if(ee(e)){this._renderPromise(e);return}else e=String(e),this._renderText(e);this.promise=void 0,this.value=e}}_renderText(e){this.value!==e&&(u(this.value)?this.node.data=String(e):this._renderNode(document.createTextNode(String(e))))}_renderTemplateResult(e){if(this.instance?.template===e.template)this.instance.render(e.values);else{let t=new O(e.template,this.options);t.render(e.values),this._renderNode(t.fragment),this.instance=t}}_renderIterable(t){let n=this.iterableParts;(this.node!==m||!n)&&(this.clear(),this.node=m,n=this.iterableParts=[]);let r=0,i=this.afterNode?this.afterNode.previousSibling:this.parentNode.lastChild;for(let o of t){let t=n[r];if(t===void 0){let r=a();this.parentNode.insertBefore(r,this.afterNode),t=new e({node:i,options:this.options}),n.push(t),i=r}t.render(o),r++}if(r===0)o(this.beforeNode,this.afterNode);else if(r<n.length){let e=n[r-1];o(e.afterNode,this.afterNode)}n.length=r}_renderNode(e){this.node!==e&&(this.clear(),this.parentNode.insertBefore(e,this.afterNode),this.node=e)}_renderPromise(e){this.promise!==e&&(this.promise=e,e.then(t=>{this.promise===e&&(this.promise=void 0,this.render(t))}))}clear(){o(this.beforeNode,this.afterNode,this.node instanceof DocumentFragment?this.node:void 0),this.node=p,this.instance=this.iterableParts=void 0}},g=class{node;constructor({node:e}){this.node=e}render(e){this.node.textContent=e==null?``:String(e)}},_=class{node;value;type;name;options;_render;constructor({node:e,attribute:t,options:n}){switch(this.node=e,this.value=f,this.options=n,t[0]){case`.`:this.type=`property`,this._render=this._renderProperty;break;case`?`:this.type=`boolean`,this._render=this._renderBoolean;break;case`@`:this.type=`event`,this._render=this._renderEvent;break;default:this.type=`attribute`,this._render=this._renderAttribute}this.name=this.type===`attribute`?t:t.slice(1),this.type===`event`&&this.node.addEventListener(this.name,this)}render(e){c(e)?e(this):e!==f&&this._render(e)}_renderProperty(e){this.node[this.name]=e}_renderBoolean(e){this.value!==!!e&&(e?this.node.setAttribute(this.name,``):this.node.removeAttribute(this.name),this.value=!!e)}_renderEvent(e){this.value=e}handleEvent(e){let t=this.value;typeof t==`function`?t.call(this.options?.host??this.node,e):t?.handleEvent?.(e)}_renderAttribute(e){this.value!==e&&(this.node.setAttribute(this.name,String(e??``)),this.value=e)}},te=/[ \x09\x0a\x0c\x0d]([^\0-\x1F\x7F-\x9F \x09\x0a\x0c\x0d"'>=/]+)[ \x09\x0a\x0c\x0d]*=$/,v=(a,o)=>{let s=[],c=(t,n)=>{if(t.includes(e))throw Error(`Parts are not allowed inside <${n?.localName}> elements`)},l=(e,o)=>{if(e instanceof Comment)e.data===n?s.push({type:g,path:o}):e.data===i&&(e.data=``,s.push({type:h,path:o}));else if(e instanceof Text)c(e.data,e.parentElement);else{if(e instanceof Element){if(e.hasAttribute(r))throw Error(`The '>' character is not allowed in attribute literals. Replace with '&gt;'`);if(e.localName===`template`&&c(e.innerHTML,e),e.hasAttribute(t)){e.removeAttribute(t);let n=[...e.attributes].filter(e=>e.value===t);for(let t of n){e.removeAttributeNode(t);let n=te.exec(a[s.length])[1];s.push({type:_,path:o,attribute:n})}}}let n=e.childNodes,i=n.length;for(let e=0;e<i;e++)l(n[e],[...o,e])}};if(l(o.content,[]),s.length<a.length-1)throw Error("Double attribute assignments are not allowed: '<div a=${0} a=${0}>'");return s},y=`${t} ${t}`,b=`--><!--${n}--><!-- `,x=`<!--${i}-->`,S=/<[a-zA-Z/!?]/g,C=(e,t)=>{let n=e.lastIndexOf(`<!--`);if((t===b||n>-1)&&e.indexOf(`-->`,n+1)<0)return b;let r=e.lastIndexOf(`>`);return S.lastIndex=r+1,S.test(e)?y:r>-1?x:t},w=e=>{let t=``,n=x;for(let r of e.slice(0,-1)){if(n=C(r,n),n===y&&!r.endsWith(`=`))throw Error("Only bare attribute parts are allowed: `<div a=${0}>`");t+=r+n}return t+e.at(-1)},T=(e,t)=>{let n=document.createElement(`template`),r=w(e);if(t){n.innerHTML=`<svg>${r}</svg>`;let e=n.content.firstChild;e.replaceWith(...e.childNodes)}else n.innerHTML=r;let o=n.content.lastChild;return o instanceof Comment&&o.data===i&&n.content.append(a()),n},E=/* @__PURE__ */ new WeakMap,ne=class{strings;element;parts;constructor(e,t){this.strings=e,this.element=T(e,t),this.parts=v(e,this.element)}},D=class{strings;values;isSvg;_template;constructor(e,t,n){this.strings=e,this.values=t,this.isSvg=n,this._template=void 0}get template(){if(this._template)return this._template;let e=E.get(this.strings);return e||(e=new ne(this.strings,this.isSvg),E.set(this.strings,e)),this._template=e,e}},O=class{template;fragment;parts;constructor(e,t){this.template=e,this.fragment=document.importNode(e.element.content,!0),this.parts=e.parts.map(e=>{let n=this.fragment;return e.path.forEach(e=>{n=n.childNodes[e]}),`attribute`in e?new _({node:n,attribute:e.attribute,options:t}):e.type===g?new g({node:n}):new h({node:n,options:t})})}render(e){this.parts.forEach((t,n)=>t.render(e[n]))}},k=/* @__PURE__ */ new WeakMap,A=e=>l(t=>{let n=k.get(t);n||(n=/* @__PURE__ */ new Map,k.set(t,n));let r=e instanceof D?n.get(e.template):void 0;r&&t.instance!==r&&(t._renderNode(r.fragment),t.instance=r),t.render(e),t.instance&&n.set(t.instance.template,t.instance)}),j=/* @__PURE__ */ new WeakMap,re=(e,t)=>l(n=>{let r=j.get(n);(!r||r.length!==e.length||e.some((e,t)=>e!==r[t]))&&(j.set(n,[...e]),n.render(t()))}),ie=e=>l(t=>{e==null&&t instanceof _&&t.type===`attribute`?(t.node.removeAttribute(t.name),t.value=f):t.render(e)}),M=/* @__PURE__ */ new WeakMap,N=({beforeNode:e,afterNode:t})=>{let n=document.createRange();return n.setStartBefore(e),n.setEndAfter(t),n},P=(e,t,n)=>l(r=>{if(!n){r.render(Array.from(e,t));return}let i=M.get(r);(!i||r.node!==i)&&(r.clear(),r.value=r.promise=void 0,i=[]);let o=/* @__PURE__ */ new Map;for(let{key:e,itemPart:t}of i)o.has(e)?N(t).deleteContents():o.set(e,t);let s=r.parentNode,c=[],l=r.beforeNode.nextSibling,u=0;for(let i of e){let e=t(i,u),d=o.get(e);if(d)o.delete(e),d.beforeNode!==l&&s.insertBefore(N(d).extractContents(),l);else{let e=a();s.insertBefore(e,l),s.insertBefore(a(),l),d=new h({node:e,options:r.options})}d.render(n(i,u++)),c.push({key:e,itemPart:d}),l=d.afterNode.nextSibling}o.forEach(e=>N(e).deleteContents()),M.set(r,c),r.node=c}),F=/* @__PURE__ */ new WeakMap,I=e=>l(t=>{let n=F.get(t);if(!n||n.htmlString!==e||t.node!==n.fragment){let n=document.createElement(`template`);n.innerHTML=e;let r=document.importNode(n.content,!0);t.render(r),F.set(t,{htmlString:e,fragment:r})}}),L=/* @__PURE__ */ new WeakMap,R=(e,t)=>l(n=>{L.has(e)?n.render(L.get(e)):(e.then(t=>L.set(e,t),()=>{}),n.render(t),n.render(e))}),z=(e,t,n)=>l(r=>{e?r.render(t):r.render(n)}),B=/* @__PURE__ */ new WeakMap,V=(e,...t)=>new D(e,t),H=(e,...t)=>new D(e,t,!0),U=(e,t,n)=>{let r=B.get(t);if(!r){let e=a();t.replaceChildren(e),r=new h({node:e,options:n}),B.set(t,r)}r.render(e)},W={fromAttribute(e,t){switch(t){case Boolean:return e!==null;case Number:return e===null?null:Number(e);case Array:case Object:return e===null?null:JSON.parse(e);default:return e}},toAttribute(e,t){switch(t){case Boolean:return e?``:null;case Array:case Object:return e==null?null:JSON.stringify(e);default:return e==null?null:String(e)}}},G=(e,t)=>!Object.is(e,t),K=e=>e.replace(/([a-z](?=[A-Z]))|([A-Z](?=[A-Z][a-z]))/g,`$1$2-`).toLowerCase(),q=(e,{attribute:t})=>t?K(e):void 0,J=(e,...t)=>e.reduce((e,n,r)=>e+String(t[r-1])+n),Y=/* @__PURE__ */ new Map,ae=e=>{if(typeof e!=`string`)return e;let t=Y.get(e);return t||(t=new CSSStyleSheet,t.replaceSync(e),Y.set(e,t)),t},oe=e=>typeof e==`string`?e:Array.from(e.cssRules,e=>e.cssText).join(`
3
+ `),X=/* @__PURE__ */ new WeakMap,Z=/* @__PURE__ */ new WeakSet,se=(e,t)=>{let n;for(let r=e;r!==$.prototype&&!n;r=Object.getPrototypeOf(r))n=Object.getOwnPropertyDescriptor(r,t);if(n?.set&&Z.has(n.set))return;let r=Symbol(t),i=n?.get??function(){return this[r]},a=n?.set??function(e){this[r]=e},o=function(e){let n=i.call(this);a.call(this,e),this.requestUpdate(t,n)};Z.add(o),Object.defineProperty(e,t,{get:i,set:o,configurable:!0,enumerable:!0})},Q=e=>{let t=X.get(e);if(t)return t;let n=e===$?void 0:Q(Object.getPrototypeOf(e));if(t={properties:new Map(n?.properties),attributes:new Map(n?.attributes)},X.set(e,t),Object.hasOwn(e,`properties`))for(let[n,r]of Object.entries(e.properties)){t.properties.set(n,r);for(let[e,r]of t.attributes)r===n&&t.attributes.delete(e);let i=q(n,r);i&&t.attributes.set(i,n),se(e.prototype,n)}return t},ce=e=>{let t=Q(e);return t.styles??=[].concat(e.styles??[])},le=(e,t,n=customElements)=>{let r=n.get(t);return r?r!==e&&console.warn(`<${t}> is already defined with a different class, the existing definition is kept`):n.define(t,e),e},$=class extends HTMLElement{static properties={};static styles;static shadowRootOptions={mode:`open`};static get observedAttributes(){return[...Q(this).attributes.keys()]}renderRoot;isUpdatePending=!1;hasUpdated=!1;#e=/* @__PURE__ */ new Map;#t=/* @__PURE__ */ new Set;#n;#r;#i;#a=!1;#o;#s=new AbortController;constructor(){super(),Q(this.constructor),this.#r=new Promise(e=>this.#i=e),this.renderRoot=this.createRenderRoot(),this.#u(),this.requestUpdate()}createRenderRoot(){return this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions)}get disconnectSignal(){return this.#s.signal}get updateComplete(){return this.#r}connectedCallback(){if(this.#s.signal.aborted&&(this.#s=new AbortController),!this.#a){this.#a=!0,this.#l();for(let e of Q(this.constructor).properties.keys())this.requestUpdate(e,void 0);this.#i(!0)}}disconnectedCallback(){this.#s.abort()}attributeChangedCallback(e,t,n){let r=Q(this.constructor),i=r.attributes.get(e);if(i===void 0||this.#n===i)return;let a=r.properties.get(i),o=a.converter?.fromAttribute??W.fromAttribute;this.#n=i,this[i]=o(n,a.type),this.#n=void 0}requestUpdate(e,t){if(e!==void 0){let n=Q(this.constructor).properties.get(e)??{};if(!(n.hasChanged??G)(this[e],t))return;this.#e.has(e)||this.#e.set(e,t),n.reflect&&this.#n!==e&&this.#t.add(e)}this.isUpdatePending||(this.isUpdatePending=!0,this.#r=this.#c(this.#r))}performUpdate(){if(!this.isUpdatePending)return;let e=this.#e,t=!1;try{t=this.shouldUpdate(e),t&&(this.willUpdate(e),this.update(e))}finally{this.#e=/* @__PURE__ */ new Map,this.isUpdatePending=!1}t&&(this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(e)),this.updated(e))}shouldUpdate(e){return!0}willUpdate(e){}update(e){for(let e of this.#t)this.#d(e);this.#t.clear();let t=this.render();if(t!==f&&(U(t,this.renderRoot,{host:this}),this.#o!==void 0)){let e=document.createElement(`style`);e.textContent=this.#o,this.renderRoot.prepend(e),this.#o=void 0}}render(){return f}firstUpdated(e){}updated(e){}async#c(e){await e;try{this.performUpdate()}catch(e){Promise.reject(e)}return!this.isUpdatePending}#l(){let e=Object.getPrototypeOf(this),t=this;for(let n of Object.getOwnPropertyNames(this))if(n in e){let e=t[n];delete t[n],t[n]=e}}#u(){let e=this.renderRoot,t=ce(this.constructor);e instanceof ShadowRoot&&t.length!==0&&(`adoptedStyleSheets`in e?e.adoptedStyleSheets=t.map(ae):this.#o=t.map(oe).join(`
4
+ `))}#d(e){let t=Q(this.constructor).properties.get(e),n=q(e,t);if(n===void 0)return;let r=(t.converter?.toAttribute??W.toAttribute)(this[e],t.type);this.#n=e,r===null?this.removeAttribute(n):this.setAttribute(n,r),this.#n=void 0}};export{$ as GluonElement,A as cache,J as css,le as define,re as guard,V as html,ie as ifDefined,f as noChange,U as render,P as repeat,H as svg,I as unsafeHTML,R as until,z as when};
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2026 Goffert van Gool
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ export * from "lite-html";
7
+ export { GluonElement, define } from "./gluon.js";
8
+ export { type PropertyType, type AttributeConverter, type PropertyDeclaration, type PropertyDeclarations, type PropertyValues, } from "./properties.js";
9
+ export { type StyleSheet, type Styles, css } from "./styles.js";
package/dist/index.js ADDED
@@ -0,0 +1,254 @@
1
+ /** @license Copyright (c) 2026 Goffert van Gool, SPDX-License-Identifier: MIT */
2
+ import { noChange, render } from "lite-html";
3
+
4
+ export * from "lite-html"
5
+
6
+ //#region src/properties.ts
7
+ var defaultConverter = {
8
+ fromAttribute(value, type) {
9
+ switch (type) {
10
+ case Boolean: return value !== null;
11
+ case Number: return value === null ? null : Number(value);
12
+ case Array:
13
+ case Object: return value === null ? null : JSON.parse(value);
14
+ default: return value;
15
+ }
16
+ },
17
+ toAttribute(value, type) {
18
+ switch (type) {
19
+ case Boolean: return value ? "" : null;
20
+ case Array:
21
+ case Object: return value == null ? null : JSON.stringify(value);
22
+ default: return value == null ? null : String(value);
23
+ }
24
+ }
25
+ };
26
+ var notEqual = (value, oldValue) => !Object.is(value, oldValue);
27
+ var camelToKebab = (camel) => camel.replace(/([a-z](?=[A-Z]))|([A-Z](?=[A-Z][a-z]))/g, "$1$2-").toLowerCase();
28
+ var attributeName = (name, { attribute }) => attribute ? camelToKebab(name) : void 0;
29
+
30
+ //#endregion
31
+ //#region src/styles.ts
32
+ var css = (strings, ...values) => strings.reduce((result, string, index) => result + String(values[index - 1]) + string);
33
+ var styleSheets = /* @__PURE__ */ new Map();
34
+ var toStyleSheet = (style) => {
35
+ if (typeof style !== "string") return style;
36
+ let sheet = styleSheets.get(style);
37
+ if (!sheet) {
38
+ sheet = new CSSStyleSheet();
39
+ sheet.replaceSync(style);
40
+ styleSheets.set(style, sheet);
41
+ }
42
+ return sheet;
43
+ };
44
+ var toCSSText = (style) => typeof style === "string" ? style : Array.from(style.cssRules, (rule) => rule.cssText).join("\n");
45
+
46
+ //#endregion
47
+ //#region src/gluon.ts
48
+ var classInfos = /* @__PURE__ */ new WeakMap();
49
+ var reactiveSetters = /* @__PURE__ */ new WeakSet();
50
+ var defineAccessor = (prototype, name) => {
51
+ let descriptor;
52
+ for (let object = prototype; object !== GluonElement.prototype && !descriptor; object = Object.getPrototypeOf(object)) descriptor = Object.getOwnPropertyDescriptor(object, name);
53
+ if (descriptor?.set && reactiveSetters.has(descriptor.set)) return;
54
+ const key = Symbol(name);
55
+ const get = descriptor?.get ?? function() {
56
+ return this[key];
57
+ };
58
+ const set = descriptor?.set ?? function(value) {
59
+ this[key] = value;
60
+ };
61
+ const reactiveSet = function(value) {
62
+ const oldValue = get.call(this);
63
+ set.call(this, value);
64
+ this.requestUpdate(name, oldValue);
65
+ };
66
+ reactiveSetters.add(reactiveSet);
67
+ Object.defineProperty(prototype, name, {
68
+ get,
69
+ set: reactiveSet,
70
+ configurable: true,
71
+ enumerable: true
72
+ });
73
+ };
74
+ var finalize = (ctor) => {
75
+ let info = classInfos.get(ctor);
76
+ if (info) return info;
77
+ const inherited = ctor === GluonElement ? void 0 : finalize(Object.getPrototypeOf(ctor));
78
+ info = {
79
+ properties: new Map(inherited?.properties),
80
+ attributes: new Map(inherited?.attributes)
81
+ };
82
+ classInfos.set(ctor, info);
83
+ if (Object.hasOwn(ctor, "properties")) for (const [name, declaration] of Object.entries(ctor.properties)) {
84
+ info.properties.set(name, declaration);
85
+ for (const [attribute, property] of info.attributes) if (property === name) info.attributes.delete(attribute);
86
+ const attribute = attributeName(name, declaration);
87
+ if (attribute) info.attributes.set(attribute, name);
88
+ defineAccessor(ctor.prototype, name);
89
+ }
90
+ return info;
91
+ };
92
+ var getStyles = (ctor) => {
93
+ const info = finalize(ctor);
94
+ return info.styles ??= [].concat(ctor.styles ?? []);
95
+ };
96
+ var define = (ctor, name, registry = customElements) => {
97
+ const existing = registry.get(name);
98
+ if (!existing) registry.define(name, ctor);
99
+ else if (existing !== ctor) console.warn(`<${name}> is already defined with a different class, the existing definition is kept`);
100
+ return ctor;
101
+ };
102
+ var GluonElement = class extends HTMLElement {
103
+ static properties = {};
104
+ static styles;
105
+ static shadowRootOptions = { mode: "open" };
106
+ static get observedAttributes() {
107
+ return [...finalize(this).attributes.keys()];
108
+ }
109
+ renderRoot;
110
+ isUpdatePending = false;
111
+ hasUpdated = false;
112
+ #changed = /* @__PURE__ */ new Map();
113
+ #reflecting = /* @__PURE__ */ new Set();
114
+ #reflectingProperty;
115
+ #updatePromise;
116
+ #enableUpdating;
117
+ #connected = false;
118
+ #styleText;
119
+ #disconnect = new AbortController();
120
+ constructor() {
121
+ super();
122
+ finalize(this.constructor);
123
+ this.#updatePromise = new Promise((resolve) => this.#enableUpdating = resolve);
124
+ this.renderRoot = this.createRenderRoot();
125
+ this.#adoptStyles();
126
+ this.requestUpdate();
127
+ }
128
+ createRenderRoot() {
129
+ return this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);
130
+ }
131
+ get disconnectSignal() {
132
+ return this.#disconnect.signal;
133
+ }
134
+ get updateComplete() {
135
+ return this.#updatePromise;
136
+ }
137
+ connectedCallback() {
138
+ if (this.#disconnect.signal.aborted) this.#disconnect = new AbortController();
139
+ if (!this.#connected) {
140
+ this.#connected = true;
141
+ this.#upgradeProperties();
142
+ for (const name of finalize(this.constructor).properties.keys()) this.requestUpdate(name, void 0);
143
+ this.#enableUpdating(true);
144
+ }
145
+ }
146
+ disconnectedCallback() {
147
+ this.#disconnect.abort();
148
+ }
149
+ attributeChangedCallback(name, _oldValue, value) {
150
+ const info = finalize(this.constructor);
151
+ const property = info.attributes.get(name);
152
+ if (property === void 0 || this.#reflectingProperty === property) return;
153
+ const declaration = info.properties.get(property);
154
+ const fromAttribute = declaration.converter?.fromAttribute ?? defaultConverter.fromAttribute;
155
+ this.#reflectingProperty = property;
156
+ this[property] = fromAttribute(value, declaration.type);
157
+ this.#reflectingProperty = void 0;
158
+ }
159
+ requestUpdate(name, oldValue) {
160
+ if (name !== void 0) {
161
+ const declaration = finalize(this.constructor).properties.get(name) ?? {};
162
+ if (!(declaration.hasChanged ?? notEqual)(this[name], oldValue)) return;
163
+ if (!this.#changed.has(name)) this.#changed.set(name, oldValue);
164
+ if (declaration.reflect && this.#reflectingProperty !== name) this.#reflecting.add(name);
165
+ }
166
+ if (!this.isUpdatePending) {
167
+ this.isUpdatePending = true;
168
+ this.#updatePromise = this.#enqueueUpdate(this.#updatePromise);
169
+ }
170
+ }
171
+ performUpdate() {
172
+ if (!this.isUpdatePending) return;
173
+ const changed = this.#changed;
174
+ let shouldUpdate = false;
175
+ try {
176
+ shouldUpdate = this.shouldUpdate(changed);
177
+ if (shouldUpdate) {
178
+ this.willUpdate(changed);
179
+ this.update(changed);
180
+ }
181
+ } finally {
182
+ this.#changed = /* @__PURE__ */ new Map();
183
+ this.isUpdatePending = false;
184
+ }
185
+ if (shouldUpdate) {
186
+ if (!this.hasUpdated) {
187
+ this.hasUpdated = true;
188
+ this.firstUpdated(changed);
189
+ }
190
+ this.updated(changed);
191
+ }
192
+ }
193
+ shouldUpdate(_changed) {
194
+ return true;
195
+ }
196
+ willUpdate(_changed) {}
197
+ update(_changed) {
198
+ for (const name of this.#reflecting) this.#propertyToAttribute(name);
199
+ this.#reflecting.clear();
200
+ const result = this.render();
201
+ if (result !== noChange) {
202
+ render(result, this.renderRoot, { host: this });
203
+ if (this.#styleText !== void 0) {
204
+ const style = document.createElement("style");
205
+ style.textContent = this.#styleText;
206
+ this.renderRoot.prepend(style);
207
+ this.#styleText = void 0;
208
+ }
209
+ }
210
+ }
211
+ render() {
212
+ return noChange;
213
+ }
214
+ firstUpdated(_changed) {}
215
+ updated(_changed) {}
216
+ async #enqueueUpdate(previous) {
217
+ await previous;
218
+ try {
219
+ this.performUpdate();
220
+ } catch (error) {
221
+ Promise.reject(error);
222
+ }
223
+ return !this.isUpdatePending;
224
+ }
225
+ #upgradeProperties() {
226
+ const prototype = Object.getPrototypeOf(this);
227
+ const properties = this;
228
+ for (const name of Object.getOwnPropertyNames(this)) if (name in prototype) {
229
+ const value = properties[name];
230
+ delete properties[name];
231
+ properties[name] = value;
232
+ }
233
+ }
234
+ #adoptStyles() {
235
+ const root = this.renderRoot;
236
+ const styles = getStyles(this.constructor);
237
+ if (!(root instanceof ShadowRoot) || styles.length === 0) return;
238
+ if ("adoptedStyleSheets" in root) root.adoptedStyleSheets = styles.map(toStyleSheet);
239
+ else this.#styleText = styles.map(toCSSText).join("\n");
240
+ }
241
+ #propertyToAttribute(name) {
242
+ const declaration = finalize(this.constructor).properties.get(name);
243
+ const attribute = attributeName(name, declaration);
244
+ if (attribute === void 0) return;
245
+ const value = (declaration.converter?.toAttribute ?? defaultConverter.toAttribute)(this[name], declaration.type);
246
+ this.#reflectingProperty = name;
247
+ if (value === null) this.removeAttribute(attribute);
248
+ else this.setAttribute(attribute, value);
249
+ this.#reflectingProperty = void 0;
250
+ }
251
+ };
252
+
253
+ //#endregion
254
+ export { GluonElement, css, define };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2026 Goffert van Gool
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ /**
7
+ * The type of a declared property, used by the default converter to read and write attributes
8
+ */
9
+ export type PropertyType = StringConstructor | NumberConstructor | BooleanConstructor | ArrayConstructor | ObjectConstructor;
10
+ /**
11
+ * Converts between a property value and its attribute value
12
+ *
13
+ * `fromAttribute` is called with `null` when the attribute is removed
14
+ * `toAttribute` returns `null` to remove the attribute
15
+ */
16
+ export interface AttributeConverter {
17
+ fromAttribute?: (value: string | null, type?: PropertyType) => unknown;
18
+ toAttribute?: (value: unknown, type?: PropertyType) => string | null;
19
+ }
20
+ /**
21
+ * The declaration of a reactive property in the static `properties` map of an element class
22
+ */
23
+ export interface PropertyDeclaration {
24
+ /** The type of the property, as a hint for the default converter. Defaults to `String` */
25
+ type?: PropertyType;
26
+ /** Whether the property is set from the attribute with its kebab-cased name. Defaults to `false` */
27
+ attribute?: boolean;
28
+ /** Whether the property value is written to its attribute when it changes, with `attribute`. Defaults to `false` */
29
+ reflect?: boolean;
30
+ /** A converter between the property value and its attribute. Defaults to the built-in converter */
31
+ converter?: AttributeConverter;
32
+ /** Decides whether a new value counts as a change. Defaults to `!Object.is(value, oldValue)` */
33
+ hasChanged?: (value: unknown, oldValue: unknown) => boolean;
34
+ }
35
+ export type PropertyDeclarations = Record<string, PropertyDeclaration>;
36
+ /**
37
+ * A map of the changed properties to their previous values, passed to the update lifecycle methods
38
+ *
39
+ * The keys are typed as the properties of the element, so `changed.has('count')` is checked
40
+ */
41
+ export interface PropertyValues<T = unknown> extends Map<PropertyKey, unknown> {
42
+ get<K extends keyof T>(key: K): T[K] | undefined;
43
+ has<K extends keyof T>(key: K): boolean;
44
+ set<K extends keyof T>(key: K, value: T[K]): this;
45
+ delete<K extends keyof T>(key: K): boolean;
46
+ }
47
+ /**
48
+ * The converter that is used for properties without their own
49
+ *
50
+ * `String` properties are copied, `Number` properties are parsed with `Number()`,
51
+ * `Boolean` properties are `true` when the attribute is present, and `Array` and `Object` properties are JSON
52
+ * A property that is `null` or `undefined` removes its attribute
53
+ */
54
+ export declare const defaultConverter: Required<AttributeConverter>;
55
+ /**
56
+ * The change detection that is used for properties without their own
57
+ */
58
+ export declare const notEqual: (value: unknown, oldValue: unknown) => boolean;
59
+ export declare const camelToKebab: (camel: string) => string;
60
+ export declare const attributeName: (name: string, { attribute }: PropertyDeclaration) => string | undefined;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2026 Goffert van Gool
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ /**
7
+ * A stylesheet for an element: CSS text, or a constructed `CSSStyleSheet`
8
+ */
9
+ export type StyleSheet = string | CSSStyleSheet;
10
+ export type Styles = StyleSheet | readonly StyleSheet[];
11
+ /**
12
+ * Tagging function for CSS in template string literals
13
+ *
14
+ * Returns the CSS as a string, for the static `styles` of an element. The tag marks the string as CSS
15
+ * for editors, formatters, and linters. Interpolated values are converted to strings, so other styles
16
+ * and numbers can be composed into it.
17
+ */
18
+ export declare const css: (strings: TemplateStringsArray, ...values: unknown[]) => string;
19
+ /**
20
+ * Returns a constructed stylesheet for a style, sharing one sheet per CSS string
21
+ */
22
+ export declare const toStyleSheet: (style: StyleSheet) => CSSStyleSheet;
23
+ /**
24
+ * Returns the CSS text of a style, for a `<style>` element
25
+ */
26
+ export declare const toCSSText: (style: StyleSheet) => string;
package/package.json CHANGED
@@ -1,66 +1,66 @@
1
1
  {
2
2
  "name": "@gluon/gluon",
3
- "version": "2.5.2",
4
- "description": "A tiny WebComponent library",
5
- "main": "gluon.js",
6
- "umd:main": "gluon.umd.js",
7
- "module": "gluon.js",
8
- "jsnext:main": "gluon.js",
3
+ "version": "3.0.0",
4
+ "description": "A tiny base class for Web Components, rendered with lite-html",
5
+ "license": "MIT",
6
+ "type": "module",
9
7
  "files": [
10
- "gluon.*"
8
+ "dist",
9
+ "CHANGELOG.md"
11
10
  ],
11
+ "main": "./dist/index.js",
12
+ "module": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "unpkg": "dist/gluon.min.js",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js",
19
+ "default": "./dist/index.js"
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "scripts": {
24
+ "dev": "vite serve dev",
25
+ "build": "vite build && vite build --mode bundle && tsc -p tsconfig.build.json",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
28
+ "typecheck": "tsc --noEmit",
29
+ "format": "prettier --write .",
30
+ "format:check": "prettier --check .",
31
+ "check": "npm run typecheck && npm run format:check && npm test",
32
+ "prepublishOnly": "npm run check && npm run build"
33
+ },
12
34
  "repository": {
13
35
  "type": "git",
14
36
  "url": "git+https://github.com/ruphin/gluonjs.git"
15
37
  },
38
+ "homepage": "https://github.com/ruphin/gluonjs",
39
+ "bugs": {
40
+ "url": "https://github.com/ruphin/gluonjs/issues"
41
+ },
16
42
  "author": "Goffert van Gool <ruphin@ruphin.net>",
17
43
  "keywords": [
18
44
  "web-components",
19
45
  "webcomponents",
20
- "lit-html",
21
- "lit"
46
+ "lite-html",
47
+ "custom-elements"
22
48
  ],
23
- "license": "MIT",
24
49
  "publishConfig": {
25
50
  "access": "public"
26
51
  },
27
- "bugs": {
28
- "url": "https://github.com/ruphin/gluonjs/issues"
52
+ "engines": {
53
+ "node": ">=20"
29
54
  },
30
- "scripts": {
31
- "build": "rollup -c && cp src/gluon.js gluon.js",
32
- "dev": "browser-sync start --config browsersync.config.js",
33
- "release": "np",
34
- "test": "karma start",
35
- "guard": "karma start --no-single-run"
36
- },
37
- "homepage": "https://github.com/ruphin/gluonjs",
38
55
  "dependencies": {
39
- "lit-html": "1.0.0"
56
+ "lite-html": "^1.2.0"
40
57
  },
41
58
  "devDependencies": {
42
- "@webcomponents/webcomponentsjs": "2.2.7",
43
- "babel-core": "6.26.3",
44
- "babel-plugin-external-helpers": "6.22.0",
45
- "babel-preset-env": "1.7.0",
46
- "babel-polyfill": "6.26.0",
47
- "browser-sync": "2.26.3",
48
- "chai": "4.2.0",
49
- "karma": "3.0.0",
50
- "karma-chai": "0.1.0",
51
- "karma-chrome-launcher": "2.2.0",
52
- "karma-firefox-launcher": "1.1.0",
53
- "karma-mocha": "1.3.0",
54
- "karma-mocha-reporter": "2.2.5",
55
- "mocha": "6.0.0",
56
- "np": "4.0.2",
57
- "rollup": "1.2.2",
58
- "rollup-plugin-babel": "4.3.2",
59
- "rollup-plugin-commonjs": "9.2.0",
60
- "rollup-plugin-cleanup": "3.1.1",
61
- "rollup-plugin-filesize": "6.0.1",
62
- "rollup-plugin-includepaths": "0.2.3",
63
- "rollup-plugin-node-resolve": "4.0.0",
64
- "rollup-plugin-terser": "4.0.4"
59
+ "@types/node": "^26.6.4",
60
+ "jsdom": "^30.1.2",
61
+ "prettier": "^3.9.9",
62
+ "typescript": "^7.0.2",
63
+ "vite": "^8.3.2",
64
+ "vitest": "^5.0.3"
65
65
  }
66
66
  }