@elenajs/core 0.13.0 → 0.14.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.
Files changed (2) hide show
  1. package/README.md +1249 -0
  2. package/package.json +2 -2
package/README.md ADDED
@@ -0,0 +1,1249 @@
1
+ <div align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://elenajs.com/img/elena-dark.png" alt="Elena" width="201" height="230">
4
+ </source>
5
+ <source media="(prefers-color-scheme: light)" srcset="https://elenajs.com/img/elena.png" alt="Elena" width="201" height="230">
6
+ </source>
7
+ <img src="https://elenajs.com/img/elena.png" alt="Elena" width="201" height="230">
8
+ </picture>
9
+
10
+ ### Simple, tiny library for building Progressive Web Components.
11
+
12
+ <br/>
13
+
14
+ <a href="https://arielsalminen.com"><img src="https://img.shields.io/badge/creator-@arielle-F95B1F" alt="Creator @arielle"/></a>
15
+ <a href="https://www.npmjs.com/org/elenajs"><img src="https://img.shields.io/npm/v/@elenajs/core.svg" alt="Latest version on npm" /></a>
16
+ <a href="https://github.com/getelena/elena/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-yellow.svg" alt="Elena is released under the MIT license." /></a>
17
+ <a href="https://github.com/getelena/elena/actions/workflows/tests.yml"><img src="https://img.shields.io/badge/coverage-100%25-green" alt="Coverage 100%" /></a>
18
+ <a href="https://www.npmjs.com/package/@elenajs/core"><img src="https://img.shields.io/npm/dt/@elenajs/core.svg" alt="Total Downloads"></a>
19
+ <a href="https://github.com/getelena/elena/actions/workflows/tests.yml"><img src="https://github.com/getelena/elena/actions/workflows/tests.yml/badge.svg" alt="Tests status" /></a>
20
+
21
+ </div>
22
+
23
+ <br/>
24
+
25
+ <p align="center"><a href="https://elenajs.com">Elena</a> is a simple, tiny library (2kB) for building <a href="#what-is-a-progressive-web-component">Progressive Web Components</a>. With Elena, you can immediately render the component’s base HTML & CSS, then progressively enhance the experience with JavaScript rather than relying on it from the start. This approach provides great support for <a href="#server-side-rendering">Server Side Rendering</a> <em>(and e.g. React Server&nbsp;Components)</em> without additional configuration or tooling.</p>
26
+
27
+ <br/>
28
+
29
+ ## Elena features
30
+
31
+ - 🔋 **Extremely lightweight:** Only 2kB minified & gzipped with zero runtime overhead.
32
+ - 📈 **Progressively enhanced:** Renders HTML & CSS first, then hydrates with JavaScript.
33
+ - 🫶 **Accessible by default:** Semantic HTML foundation with no Shadow DOM barriers.
34
+ - 🌍 **Standards based:** Built entirely on native custom elements & web standards.
35
+ - ⚡ **Reactive props:** Prop changes sync to attributes and trigger updates automatically.
36
+ - 🎨 **Scoped styles:** Simple & clean CSS encapsulation without complex workarounds.
37
+ - 🖥️ **SSR friendly:** Works out of the box, with optional server-side utilities if needed.
38
+ - 🧩 **Zero dependencies:** No runtime dependencies, runs entirely on the web platform.
39
+ - 🔓 **Zero lock-in:** Works with every major framework, or no framework at all.
40
+
41
+ <br/>
42
+
43
+ ## Table of contents
44
+
45
+ - **[Design principles](#design-principles)**
46
+ - **[What is a Progressive Web Component?](#what-is-a-progressive-web-component)**
47
+ - **[Getting started](#getting-started)**
48
+ - **[Quick start](#quick-start)**
49
+ - **[Installation](#installation)**
50
+ - **[Creating a component](#create-a-composite-component)**
51
+ - **[Options](#options)**
52
+ - **[Props](#props)**
53
+ - **[Reflecting props to attributes](#reflecting-props-to-attributes)**
54
+ - **[Documenting props](#documenting-props)**
55
+ - **[Prop types](#prop-types)**
56
+ - **[Events](#events)**
57
+ - **[Methods](#methods)**
58
+ - **[Utility methods](#utility-methods)**
59
+ - **[Custom methods](#custom-methods)**
60
+ - **[Templates](#templates)**
61
+ - **[`nothing`](#nothing-1)**
62
+ - **[`unsafeHTML`](#unsafehtml-1)**
63
+ - **[Element ref](#element-ref)**
64
+ - **[Text content](#text-content)**
65
+ - **[Advanced template example](#advanced-template-example)**
66
+ - **[Live demos](#live-demos)**
67
+ - **[Usage examples](#usage-examples)**
68
+ - **[Project examples](#project-examples)**
69
+ - **[Component examples](#component-examples)**
70
+ - **[Server Side Rendering](#server-side-rendering)**
71
+ - **[Avoiding layout shifts](#avoiding-layout-shifts)**
72
+ - **[Rendering Primitive Components to HTML strings](#rendering-primitive-components-to-html-strings)**
73
+ - **[Framework examples](#framework-examples)**
74
+ - **[TypeScript](#typescript)**
75
+ - **[Generating types for components](#generating-types-for-components)**
76
+ - **[Using the generated types](#using-the-generated-types)**
77
+ - **[TypeScript examples](#typescript-examples)**
78
+ - **[Authoring components with TypeScript](#authoring-components-with-typescript)**
79
+ - **[CSS styles](#css-styles)**
80
+ - **[Writing scoped styles](#writing-scoped-styles)**
81
+ - **[Elena CSS Encapsulation Pattern](#elena-css-encapsulation-pattern)**
82
+ - **[Pre-hydration state and styles](#pre-hydration-state-and-styles)**
83
+ - **[Styling Composite Components](#styling-composite-components)**
84
+ - **[Documenting public CSS properties](#documenting-public-css-properties)**
85
+ - **[Misc](#misc)**
86
+ - **[Load event](#load-event)**
87
+ - **[Hide until loaded](#hide-until-loaded)**
88
+ - **[Known issues](#known-issues)**
89
+ - **[Browser compatibility](#browser-compatibility)**
90
+ - **[JavaScript frameworks](#javascript-frameworks)**
91
+ - **[Packages in this monorepo](#packages)**
92
+ - **[Development](#development)**
93
+
94
+ <br/>
95
+
96
+ ## Design principles
97
+
98
+ - **Progressive:** Renders HTML and CSS first, hydrates it with JavaScript after.
99
+ - **Reliable:** Predictable lifecycle and property syncing with no hidden magic.
100
+ - **Interoperable:** Built on web standards; no proprietary abstractions.
101
+ - **Modular:** Small, composable pieces you can use independently.
102
+ - **Universal:** Works across frameworks, tools, and environments.
103
+ - **Lightweight:** 2kB minified & gzipped, zero runtime dependencies.
104
+ - **Accessible:** Built on semantic HTML, assistive technologies supported by default.
105
+
106
+ <br/>
107
+
108
+ ## What is a Progressive Web Component?
109
+
110
+ A _“Progressive Web Component”_ is a native Custom Element designed in two layers: a base layer of HTML and CSS that renders immediately, without JavaScript, and an enhancement layer of JavaScript that adds reactivity, event handling, and dynamic updates once it loads.
111
+
112
+ This mirrors the classic principle of [progressive enhancement](https://en.wikipedia.org/wiki/Progressive_enhancement): start from a functional baseline that works everywhere, then improve the experience for users who have JavaScript available. The result is components that render immediately, are more resilient to script failures, are naturally SSR-friendly, and also compatible with any framework.
113
+
114
+ There are two types of Progressive Web Components:
115
+
116
+ ### Primitive Components
117
+
118
+ - Self-contained components that own and render their own HTML markup.
119
+ - All content is controlled through `props`, nothing is composed into them except text content.
120
+ - Examples: `button`, `input`, `checkbox`, `radio`, `textarea`, `icon`, `spinner`, `switch`.
121
+
122
+ ### Composite Components
123
+
124
+ - Components that wrap and enhance the HTML composed inside them, including other components.
125
+ - Provide styling, layout, and behavior around the composed content.
126
+ - Examples: `stack`, `table`, `layout`, `card`, `banner`, `visually-hidden`, `fieldset`.
127
+
128
+ <br/>
129
+
130
+ ## Getting started
131
+
132
+ ### Quick start
133
+
134
+ If you just want to quickly test Elena in a web browser, the fastest way is to include the following directly into your page with a `<script>` tag:
135
+
136
+ ```html
137
+ <script type="module">
138
+ import { Elena } from "https://unpkg.com/@elenajs/core@0.13.0";
139
+
140
+ export default class MyComponent extends Elena(HTMLElement, {
141
+ tagName: "my-component"
142
+ }) {
143
+ // Do something, or leave empty.
144
+ // This is a valid Elena (composite) component as is.
145
+ }
146
+ MyComponent.define();
147
+ </script>
148
+ ```
149
+
150
+ Once created, add scoped `<styles>` for your component as well:
151
+
152
+ ```html
153
+ <style>
154
+ @scope (my-component) {
155
+ :scope {
156
+ display: inline-block;
157
+ background: pink;
158
+ color: black;
159
+ }
160
+ }
161
+ </style>
162
+ ```
163
+
164
+ Now you can use your component anywhere on the page:
165
+
166
+ ```html
167
+ <my-component>Hello Elena!</my-component>
168
+ ```
169
+
170
+ > [!TIP]
171
+ > Whilst this is the fastest way to get started, we don’t recommend it for production since you would be relying entirely on unpkg CDN. Instead, we recommend using the [@elenajs/bundler](#elenajsbundler) for production, for optimal performance.
172
+
173
+ ### Installation
174
+
175
+ To install Elena as a dependency in your project, run:
176
+
177
+ ```bash
178
+ npm install @elenajs/core
179
+ ```
180
+
181
+ Once Elena is installed, you can import it from the package:
182
+
183
+ ```js
184
+ import { Elena } from "@elenajs/core";
185
+ ```
186
+
187
+ ### Create a Composite Component
188
+
189
+ ```js
190
+ // ░ [ELENA]: Composite Component
191
+ export default class Stack extends Elena(HTMLElement, {
192
+ tagName: "elena-stack",
193
+ props: ["direction"],
194
+ }) {
195
+ constructor() {
196
+ super();
197
+ this.direction = "column";
198
+ }
199
+ // Note that Composite Components do not call render()
200
+ }
201
+ Stack.define();
202
+ ```
203
+
204
+ #### Usage:
205
+
206
+ ```html
207
+ <elena-stack>
208
+ <elena-input label="Name" type="text"></elena-input>
209
+ <elena-input label="Email" type="email"></elena-input>
210
+ <elena-textarea label="Message"></elena-textarea>
211
+ <elena-button type="submit">Submit</elena-button>
212
+ </elena-stack>
213
+ ```
214
+
215
+ ### …Or, create a Primitive Component
216
+
217
+ ```js
218
+ import { Elena, html } from "@elenajs/core";
219
+
220
+ // ░ [ELENA]: Primitive Component
221
+ export default class Button extends Elena(HTMLElement, {
222
+ tagName: "elena-button",
223
+ props: ["variant"],
224
+ }) {
225
+ constructor() {
226
+ super();
227
+ this.variant = "default";
228
+ }
229
+ // Primitive Components return their `html` in render()
230
+ render() {
231
+ return html`<button>${this.text}</button>`;
232
+ }
233
+ }
234
+ Button.define();
235
+ ```
236
+
237
+ #### Usage:
238
+
239
+ ```html
240
+ <elena-button variant="primary">Save</elena-button>
241
+ <elena-button>Cancel</elena-button>
242
+ ```
243
+
244
+ <br/>
245
+
246
+ ## Options
247
+
248
+ Elena provides an options object where you can set the following:
249
+
250
+ ```js
251
+ export default class Button extends Elena(HTMLElement, {
252
+ // Custom element tag name to register:
253
+ tagName: "elena-button",
254
+
255
+ // Props to observe and sync as attributes:
256
+ props: ["label", "disabled"],
257
+
258
+ // Events to delegate from the inner element:
259
+ events: ["click", "focus", "blur"],
260
+
261
+ // CSS selector for the inner element to be used as Ref:
262
+ element: ".my-button",
263
+ })
264
+ ```
265
+
266
+ All of Elena’s options are optional. `tagName` is required only if you want Elena to handle the web component registration for you. Otherwise call `customElements.define()` yourself:
267
+
268
+ ```js
269
+ export default class Button extends Elena(HTMLElement) {
270
+ // do something...
271
+ }
272
+ customElements.define("elena-button", Button);
273
+ ```
274
+
275
+ Please note though that doing this means that your web component can no longer be used in a server context.
276
+
277
+ > [!TIP]
278
+ > When working with Primitive Components, leaving out `element` option means that Elena will try use `firstElementChild` instead, if available. In cases when your template markup is simple, this is actually more performant when you have hundreds or even thousands of Elena components on a page.
279
+
280
+ <br/>
281
+
282
+ ## Props
283
+
284
+ Elena allows you to define prop declarations in its options object. This makes Elena aware of what external props passed to the element should be observed and synced as attributes between the web component host and the inner template element (passed as an `element` in options).
285
+
286
+ Props are declared in the `props` array in the options object, with default values set inside the `constructor`:
287
+
288
+ ```js
289
+ export default class Button extends Elena(HTMLElement, {
290
+ props: ["variant", "disabled", "value", "type"],
291
+ }) {
292
+ constructor() {
293
+ super();
294
+
295
+ this.variant = "default";
296
+ this.disabled = false;
297
+ this.value = "";
298
+ this.type = "button";
299
+ }
300
+ }
301
+ ```
302
+
303
+ > [!TIP]
304
+ > When naming properties, keep them simple, easy to understand, and a maximum of 1 word (e.g. `variant`).
305
+
306
+ ### Reflecting props to attributes
307
+
308
+ By default, Elena reflects all properties to the host element as HTML attributes. If you want to disable this feature for a specific property, use `reflect: false`:
309
+
310
+ ```js
311
+ const options = {
312
+ props: [
313
+ "variant",
314
+ "size",
315
+ { name: "icon", reflect: false },
316
+ ],
317
+ };
318
+ ```
319
+
320
+ ### Documenting props
321
+
322
+ In addition to declaring props, you can (and should!) document them using a [JSDoc style syntax](https://jsdoc.app):
323
+
324
+ ```js
325
+ /**
326
+ * The style variant of the button.
327
+ * @attribute
328
+ * @type {"default" | "primary" | "danger"}
329
+ */
330
+ this.variant = "default";
331
+
332
+ /**
333
+ * Makes the component disabled.
334
+ * @attribute
335
+ * @type {Boolean}
336
+ */
337
+ this.disabled = false;
338
+
339
+ /**
340
+ * The value used to identify the button in forms.
341
+ * @attribute
342
+ * @type {string}
343
+ */
344
+ this.value = "";
345
+
346
+ /**
347
+ * The type of the button.
348
+ * @attribute
349
+ * @type {"submit" | "reset" | "button"}
350
+ */
351
+ this.type = "button";
352
+ ```
353
+
354
+ > [!TIP]
355
+ > **`@elenajs/bundler`** transforms the above JSDocs automatically to TypeScript types and Custom Elements Manifest which allows tooling and IDEs to give rich information about the Elena elements.
356
+
357
+ ### Prop types
358
+
359
+ The `@type` can be one of the following native constructors:
360
+
361
+ ```js
362
+ /** @type {string} */
363
+ /** @type {Number} */
364
+ /** @type {Array} */
365
+ /** @type {Boolean} */
366
+ /** @type {Object} */
367
+ ```
368
+
369
+ Additionally, you can provide possible prop values using the following syntax:
370
+
371
+ ```js
372
+ /** @type {"default" | "primary" | "danger"} */
373
+ ```
374
+
375
+ <br/>
376
+
377
+ ## Events
378
+
379
+ Elena allows you to define event declarations in its options object. The `events` array is used for determining which events the element should listen to and delegate from the inner template element:
380
+
381
+ ```js
382
+ export default class Button extends Elena(HTMLElement, {
383
+ events: ["click", "focus", "blur"],
384
+ })
385
+ ```
386
+
387
+ Once declared, Elena will set up the necessary event listeners and dispatching logic and take care of cleanup when the element is removed from the DOM.
388
+
389
+ > [!TIP]
390
+ > You can alternatively build your own custom logic inside the web component for events and not rely on the built-in functionality in Elena.
391
+
392
+ <br/>
393
+
394
+ ## Methods
395
+
396
+ Elena ships with the following built-in lifecycle methods:
397
+
398
+ - **`connectedCallback()`:** Called each time the element is added to the DOM.
399
+ - **`disconnectedCallback()`:** Called each time the element is removed from the DOM.
400
+ - **`attributeChangedCallback()`:** Called when Elena’s props are changed, added, removed or replaced.
401
+ - **`render()`:** Called whenever there’s an update that needs rendering.
402
+ - **`updated()`:** Performs a post-update and adds the `hydrated` attribute to the Host element.
403
+
404
+ ### Utility methods
405
+
406
+ Additionally, Elena provides the following utility methods:
407
+
408
+ #### `ClassName.define()`
409
+
410
+ Register the web component with SSR guards. Call this on your subclass after the class body is defined. The tag name is read from the `tagName` option set when calling `Elena()`.
411
+
412
+ ```js
413
+ MyElement.define();
414
+ ```
415
+
416
+ #### `html`
417
+
418
+ Tagged template for defining an Elena web component’s HTML structure. Return it from `render()`. Dynamic values are auto-escaped, and nested `html` sub-templates pass through as trusted HTML without double-escaping:
419
+
420
+ ```js
421
+ import { Elena, html } from "@elenajs/core";
422
+
423
+ // ...later:
424
+ render() {
425
+ return html`
426
+ <button class="elena-button">
427
+ ${this.text}
428
+ </button>
429
+ `;
430
+ }
431
+ ```
432
+
433
+ #### `nothing`
434
+
435
+ A placeholder you can use in conditional template expressions when there is nothing to render. It always produces an empty string and signals to the template engine that no processing is needed.
436
+
437
+ ```js
438
+ import { Elena, html, nothing } from "@elenajs/core";
439
+
440
+ // ...later:
441
+ render() {
442
+ return html`
443
+ <button>
444
+ ${this.icon ? html`<span class="icon">${this.icon}</span>` : nothing}
445
+ ${this.text}
446
+ </button>
447
+ `;
448
+ }
449
+ ```
450
+
451
+ #### `unsafeHTML`
452
+
453
+ Values interpolated into Elena’s `html` tagged template are auto-escaped to prevent XSS. `unsafeHTML` allows you to bypass this and render a trusted HTML string without escaping, for example an SVG icon or markup from a database.
454
+
455
+ ```js
456
+ import { Elena, html, unsafeHTML, nothing } from "@elenajs/core";
457
+
458
+ // ...later:
459
+ render() {
460
+ const icon = this.icon ? unsafeHTML(`<span>${this.icon}</span>`) : nothing;
461
+ const text = this.text ? html`<span>${this.text}</span>` : nothing;
462
+
463
+ return html`
464
+ <button>
465
+ ${text}
466
+ ${icon}
467
+ </button>
468
+ `;
469
+ }
470
+ ```
471
+
472
+ ### Custom methods
473
+
474
+ You can also define your own custom methods:
475
+
476
+ ```js
477
+ export default class Button extends Elena(HTMLElement) {
478
+ /**
479
+ * Renders a link: <a href="#">.
480
+ * @internal
481
+ */
482
+ renderLink(template) {
483
+ return html`
484
+ <a
485
+ class="elena-button"
486
+ href="${this.href}"
487
+ target="${this.target}"
488
+ ${this.download ? "download" : nothing}
489
+ ${this.label ? html`aria-label="${this.label}"` : nothing}>
490
+ ${template}
491
+ </a>
492
+ `;
493
+ }
494
+ }
495
+ ```
496
+
497
+ Elena also allows you to extend the lifecycle methods by calling `super`:
498
+
499
+ ```js
500
+ export default class Button extends Elena(HTMLElement) {
501
+
502
+ connectedCallback() {
503
+ super.connectedCallback();
504
+ console.log("Element was added to the DOM.");
505
+ }
506
+
507
+ disconnectedCallback() {
508
+ super.disconnectedCallback();
509
+ console.log("Element was removed from the DOM.");
510
+ }
511
+ }
512
+ ```
513
+
514
+ <br/>
515
+
516
+ ## Templates
517
+
518
+ Elena uses an HTML-based template syntax built on JavaScript [tagged template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals). Return an `html` tagged template from `render()`:
519
+
520
+ ```js
521
+ import { Elena, html } from "@elenajs/core";
522
+
523
+ // ...later:
524
+ render() {
525
+ return html`
526
+ <button variant="${this.variant || "default"}">
527
+ ${this.text}
528
+ </button>
529
+ `;
530
+ }
531
+ ```
532
+
533
+ The content of the `html` method is passed as tagged template literals, which Elena then compiles on the fly.
534
+
535
+ ### `nothing`
536
+
537
+ A placeholder you can use in conditional template expressions when there is nothing to render. It always produces an empty string and signals to the template engine that no processing is needed.
538
+
539
+ ```js
540
+ import { Elena, html, nothing } from "@elenajs/core";
541
+
542
+ // ...later:
543
+ render() {
544
+ return html`
545
+ <button>
546
+ ${this.icon ? html`<span class="icon">${this.icon}</span>` : nothing}
547
+ ${this.text}
548
+ </button>
549
+ `;
550
+ }
551
+ ```
552
+
553
+ ### `unsafeHTML`
554
+
555
+ Values interpolated into Elena’s `html` tagged template are auto-escaped to prevent XSS. `unsafeHTML` allows you to bypass this and render a trusted HTML string without escaping, for example an SVG icon or markup from a database.
556
+
557
+ ```js
558
+ import { Elena, html, unsafeHTML, nothing } from "@elenajs/core";
559
+
560
+ // ...later:
561
+ render() {
562
+ const icon = this.icon ? unsafeHTML(`<span>${this.icon}</span>`) : nothing;
563
+ const text = this.text ? html`<span>${this.text}</span>` : nothing;
564
+
565
+ return html`
566
+ <button>
567
+ ${text}
568
+ ${icon}
569
+ </button>
570
+ `;
571
+ }
572
+ ```
573
+
574
+ ### Element ref
575
+
576
+ Elena provides a special **Ref** to the `element` you pass as a DOM selector:
577
+
578
+ ```js
579
+ export default class Button extends Elena(HTMLElement, {
580
+ element: ".my-button",
581
+ })
582
+ ```
583
+
584
+ This allows you a direct access to the underlying DOM element:
585
+
586
+ ```js
587
+ console.log(this.element);
588
+ ```
589
+
590
+ ### Text content
591
+
592
+ Every Elena element has a built-in reactive `text` property. On first connect, Elena automatically captures the element’s `textContent` from the light DOM before rendering. This lets you pass text content naturally as children:
593
+
594
+ ```html
595
+ <elena-button>Click me</elena-button>
596
+ ```
597
+
598
+ Use `this.text` in your component's `render()` method to reference the captured text:
599
+
600
+ ```js
601
+ render() {
602
+ return html`<button class="elena-button">${this.text}</button>`;
603
+ }
604
+ ```
605
+
606
+ The `text` property is reactive, setting it programmatically triggers a re-render:
607
+
608
+ ```js
609
+ const button = document.querySelector("elena-button");
610
+ button.text = "Save changes";
611
+ ```
612
+
613
+ When used with JavaScript frameworks, passing text as children works for static text:
614
+
615
+ ```jsx
616
+ // Works for static text
617
+ <elena-button>Click me</elena-button>
618
+ ```
619
+
620
+ For dynamic text that changes over time, use the `text` property instead, since **Primitive Components** own their internal DOM and frameworks cannot update children after Elena has hydrated the element:
621
+
622
+ ```jsx
623
+ // React
624
+ <elena-button text={buttonText} />
625
+
626
+ // Angular
627
+ <elena-button [text]="buttonText"></elena-button>
628
+
629
+ // Vue
630
+ <elena-button :text="buttonText"></elena-button>
631
+ ```
632
+
633
+ > [!TIP]
634
+ > **Composite Components** don’t need the above, they preserve children naturally since they have no `render()` method. This feature is for **Primitive Components** only which own their internal DOM and would otherwise destroy any children passed to them.
635
+
636
+ ### Advanced template example
637
+
638
+ ```js
639
+ import { Elena, html, nothing } from "@elenajs/core";
640
+
641
+ // ...later:
642
+ render() {
643
+ return html`
644
+ <label for="${this.identifier}">${this.label}</label>
645
+ <div class="elena-input-wrapper">
646
+ ${this.start ? html`<div class="elena-input-start">${this.start}</div>` : nothing}
647
+ <input
648
+ id="${this.identifier}"
649
+ class="elena-input ${this.start ? "elena-input-has-start" : nothing}"
650
+ />
651
+ </div>
652
+ ${this.error ? html`<div class="elena-input-error">${this.error}</div>` : nothing}
653
+ `;
654
+ }
655
+ ```
656
+
657
+ <br/>
658
+
659
+ ## Live demos
660
+
661
+ - **[Client, partial SSR](https://arielsalminen.com/elena/)**
662
+ - **[Server, full SSR](https://arielsalminen.com/elena/server.html)**
663
+
664
+ <br/>
665
+
666
+ ## Usage examples
667
+
668
+ ### Project examples
669
+
670
+ - **[Usage with Angular](https://github.com/getelena/angular-example-project)**
671
+ - **[Usage with Eleventy](https://github.com/getelena/eleventy-example-project)**
672
+ - **[Usage with HTML](https://github.com/getelena/html-example-project)**
673
+ - **[Usage with Next.js](https://github.com/getelena/next-example-project)**
674
+ - **[Usage with React](https://github.com/getelena/react-example-project)**
675
+ - **[Usage with Svelte](https://github.com/getelena/svelte-example-project)**
676
+ - **[Usage with Vue](https://github.com/getelena/vue-example-project)**
677
+
678
+ ### Component examples
679
+
680
+ #### Composite Component
681
+
682
+ Below is an example of a **Composite Component** which includes `props` and documentation:
683
+
684
+ ```js
685
+ // ░ [ELENA]: Composite Component
686
+ import { Elena } from "@elenajs/core";
687
+
688
+ const options = {
689
+ tagName: "elena-stack",
690
+ props: ["direction"],
691
+ };
692
+
693
+ /**
694
+ * Stack component manages layout of immediate children
695
+ * with optional spacing between each child.
696
+ *
697
+ * @displayName Stack
698
+ * @slot - The stacked content
699
+ * @status alpha
700
+ */
701
+ export default class Stack extends Elena(HTMLElement, options) {
702
+ constructor() {
703
+ super();
704
+
705
+ /**
706
+ * The direction of the stack.
707
+ * @attribute
708
+ * @type {"column" | "row"}
709
+ */
710
+ this.direction = "column";
711
+ }
712
+ }
713
+ Stack.define();
714
+ ```
715
+
716
+ #### Primitive Component
717
+
718
+ Below is an example of a **Primitive Component** which includes `props`, `events`, `methods` and documentation:
719
+
720
+ ```js
721
+ // ░ [ELENA]: Primitive Component
722
+ import { Elena, html } from "@elenajs/core";
723
+
724
+ const options = {
725
+ tagName: "elena-button",
726
+ props: ["variant", "disabled", "type"],
727
+ events: ["click", "focus", "blur"],
728
+ };
729
+
730
+ /**
731
+ * The description of the component goes here.
732
+ *
733
+ * @displayName Button
734
+ * @status alpha
735
+ *
736
+ * @event click - Programmatically fire click on the component.
737
+ * @event focus - Programmatically move focus to the component.
738
+ * @event blur - Programmatically remove focus from the component.
739
+ *
740
+ * @cssprop [--elena-button-text] - Overrides the default text color.
741
+ * @cssprop [--elena-button-bg] - Overrides the default background color.
742
+ * @cssprop [--elena-button-font] - Overrides the default font-family.
743
+ */
744
+ export default class Button extends Elena(HTMLElement, options) {
745
+ constructor() {
746
+ super();
747
+
748
+ /**
749
+ * The style variant of the button.
750
+ * @attribute
751
+ * @type {"default" | "primary" | "danger"}
752
+ */
753
+ this.variant = "default";
754
+
755
+ /**
756
+ * Makes the component disabled.
757
+ * @attribute
758
+ * @type {Boolean}
759
+ */
760
+ this.disabled = false;
761
+
762
+ /**
763
+ * The type of the button.
764
+ * @attribute
765
+ * @type {"submit" | "reset" | "button"}
766
+ */
767
+ this.type = "button";
768
+ }
769
+
770
+ /**
771
+ * An example custom method.
772
+ */
773
+ myMethod() {
774
+ console.log(this.element);
775
+ }
776
+
777
+ /**
778
+ * Renders the button component template.
779
+ * @internal
780
+ */
781
+ render() {
782
+ return html`
783
+ <button>${this.text}</button>
784
+ `;
785
+ }
786
+ }
787
+ Button.define();
788
+ ```
789
+
790
+ <br/>
791
+
792
+ ## Server Side Rendering
793
+
794
+ Elena’s recommended approach to Server Side Rendering (SSR) is simple & straightforward. Since [Progressive Web Components](#what-is-a-progressive-web-component) are primarily HTML & CSS, you don’t need any special logic on the server to render them. The **[Composite Components](#2-composite-components)** provide a full support for SSR by default, while the **[Primitive Components](#1-primitive-components)** provide a partial support and do the rest of the hydration on the client side.
795
+
796
+ Partial SSR support for the **Primitive Components** means that the component’s base HTML & CSS lives in the `Light DOM`. The JavaScript lifecycle is then used to progressively enhance the functionality and markup once the element is registered.
797
+
798
+ The benefit of Elena’s approach is that it doesn’t need any extra logic on the server while still allowing you to ship all your layout components _(the “Composite Components"!)_ with full SSR support.
799
+
800
+ ### Avoiding layout shifts
801
+
802
+ For the **Primitive Components** specifically, our recommendation is to ship them with CSS styles that visually matches the `loading` and `hydrated` states without causing layout shift, FOUC, or FOIC _(Flash Of Unstyled Content, Flash Of Invisible Content)._ This can be achieved utilizing the provided `hydrated` attribute in your component styles:
803
+
804
+ ```css
805
+ /* Elena SSR Pattern to avoid layout shift */
806
+ :scope:not([hydrated]),
807
+ .inner-element {
808
+ color: var(--elena-button-text);
809
+ }
810
+ ```
811
+
812
+ Since **Primitive Components** are self-contained and render their own HTML markup, you may sometimes need access to more than just the initial text content pre-hydration for better SSR support to avoid layout shifts. This can be achieved with pseudo elements in CSS by referencing the attributes set on the element itself:
813
+
814
+ ```css
815
+ :scope:not([hydrated])::before {
816
+ content: attr(label);
817
+ /* etc */
818
+ }
819
+
820
+ :scope:not([hydrated])::after {
821
+ content: attr(placeholder);
822
+ /* etc */
823
+ }
824
+ ```
825
+
826
+ > [!TIP]
827
+ > You can skip this section entirely for Composite Components, when you plan to [hide components until loaded](#hide-until-loaded), or when the rest of your app renders client side only.
828
+
829
+ ### Rendering Primitive Components to HTML strings
830
+
831
+ When you don’t want to handle the pre-hydration state with CSS, you can expand the **Primitive Component** templates inline by using the provided utility package called [@elenajs/ssr](https://github.com/getelena/elena/tree/main/packages/ssr) that renders the Elena Primitive Components to HTML strings for full SSR support.
832
+
833
+ Please see the [SSR package’s readme](https://github.com/getelena/elena/tree/main/packages/ssr) for full usage guidelines.
834
+
835
+ > [!WARNING]
836
+ > Please note that `@elenajs/ssr` is an experimental package and not yet ready for production use. APIs may change without notice.
837
+
838
+ ### Framework examples
839
+
840
+ Elena currently provides SSR examples for the following frameworks:
841
+
842
+ - **[Eleventy](https://github.com/getelena/eleventy-example-project)**
843
+ - **[Plain HTML](https://github.com/getelena/html-example-project)**
844
+ - **[Next.js](https://github.com/getelena/next-example-project)** _(Elena can even be used inside React Server Components, see [src/app/page.tsx](https://github.com/getelena/next-example-project/blob/main/src/app/page.tsx))_
845
+
846
+ <br/>
847
+
848
+ ## TypeScript
849
+
850
+ Elena is written in vanilla JavaScript with JSDoc annotations. The **`@elenajs/core`** library ships its own type declarations (`dist/elena.d.ts`) which are generated automatically by `tsc` from the JSDoc so that you get full IntelliSense and type checking.
851
+
852
+ ```ts
853
+ import { Elena, html, nothing } from "@elenajs/core";
854
+ // Elena, ElenaOptions, html, nothing are all typed
855
+ ```
856
+
857
+ ### Generating types for components
858
+
859
+ When you build your own Elena components, **`@elenajs/bundler`** can generate TypeScript declarations for each one. Running `elena build` (or calling the bundler programmatically) produces:
860
+
861
+ - **Per-component `.d.ts` files**: A declaration file for each component (e.g. `button.d.ts`) with typed props and event handlers, derived from your JSDoc annotations. This lets TypeScript resolve sub-path imports like `@my-lib/components/dist/button.js`.
862
+ - **`custom-elements.json`**: The [Custom Elements Manifest](https://custom-elements-manifest.open-wc.org/), a machine-readable description of your components used by IDEs and documentation tools.
863
+ - **`custom-elements.d.ts`**: JSX integration types that map your custom element tag names to their prop types. This enables autocomplete and type checking for `<elena-button variant="primary" />` in JSX/TSX files.
864
+
865
+ ### Using the generated types
866
+
867
+ The generated `custom-elements.d.ts` exports a `CustomElements` type map and a `ScopedElements` helper. To get type checking in JSX (this works with Next.js, see further down for more examples):
868
+
869
+ ```ts
870
+ // types.d.ts (in your consuming project)
871
+ import type { CustomElements } from "@my-lib/components";
872
+
873
+ type ElenaIntrinsicElements = {
874
+ [K in keyof CustomElements]: CustomElements[K] & {
875
+ onClick?: (e: MouseEvent) => void;
876
+ onFocus?: (e: FocusEvent) => void;
877
+ onBlur?: (e: FocusEvent) => void;
878
+ children?: React.ReactNode;
879
+ };
880
+ };
881
+
882
+ declare module "react" {
883
+ namespace JSX {
884
+ interface IntrinsicElements extends ElenaIntrinsicElements {}
885
+ }
886
+ }
887
+ ```
888
+
889
+ ### TypeScript examples
890
+
891
+ Elena provides TypeScript examples for the following JavaScript frameworks:
892
+
893
+ - **[Next.js](https://github.com/getelena/next-example-project)**
894
+ - **[React](https://github.com/getelena/react-example-project)**
895
+ - **[Svelte](https://github.com/getelena/svelte-example-project)**
896
+ - **[Vue](https://github.com/getelena/vue-example-project)**
897
+
898
+ ### Authoring components with TypeScript
899
+
900
+ When using TypeScript (instead of JavaScript) to author the Elena components, you can simplify the code (like omitting the `constructor` part) and have your type definitions inline:
901
+
902
+ ```ts
903
+ // ░ [ELENA]: Primitive Component
904
+ import { Elena, html } from "@elenajs/core";
905
+
906
+ export default class Button extends Elena(HTMLElement, {
907
+ tagName: "elena-button",
908
+ props: ["variant"],
909
+ }) {
910
+ /**
911
+ * The style variant of the component.
912
+ * @attribute
913
+ */
914
+ variant: "default" | "primary" | "danger" = "default";
915
+
916
+ /**
917
+ * Renders the html template.
918
+ * @internal
919
+ */
920
+ render() {
921
+ return html`
922
+ <button>${this.text}</button>
923
+ `;
924
+ }
925
+ }
926
+ Button.define();
927
+ ```
928
+
929
+ <br/>
930
+
931
+ ## CSS styles
932
+
933
+ These guidelines cover the approaches that we recommend when styling Progressive Web Components to make them work reliably across the lifecycle of a component. You’re obviously able to craft the CSS the best way you see fit for your purpose, but there are some things to take into account that we’ve tried to cover below.
934
+
935
+ ### Writing scoped styles
936
+
937
+ Elena recommends using the [@scope](https://caniuse.com/css-cascade-scope) at-rule which prevents the component styles from leaking to the outer page. This makes it possible to have entirely isolated styles without sacrificing inheritance or cascading:
938
+
939
+ ```css
940
+ @scope (elena-button) {
941
+ /**
942
+ * Scoped styles for the elena-button. These won’t leak
943
+ * out or affect any other elements in your app.
944
+ */
945
+ }
946
+ ```
947
+
948
+ To style the host `elena-button` itself, you can use `:scope`:
949
+
950
+ ```css
951
+ @scope (elena-button) {
952
+
953
+ /* Targets the host element (elena-button) */
954
+ :scope {
955
+ all: unset;
956
+ display: inline-block;
957
+ }
958
+ }
959
+ ```
960
+
961
+ The full baseline pattern for authoring encapsulated component styles looks like this:
962
+
963
+ ```css
964
+ /* Scope makes sure styles don’t leak out */
965
+ @scope (elena-button) {
966
+
967
+ /* Unset makes sure styles don’t leak in */
968
+ :scope, *, *::before, *::after {
969
+ all: unset;
970
+ }
971
+
972
+ /* Targets the host element (elena-button) */
973
+ :scope {
974
+
975
+ /* Public CSS properties */
976
+ --elena-button-font: sans-serif;
977
+ --elena-button-text: white;
978
+ --elena-button-bg: blue;
979
+
980
+ /* Display mode for the host element */
981
+ display: inline-block;
982
+ }
983
+
984
+ /* Elena SSR Pattern to avoid layout shift */
985
+ :scope:not([hydrated]),
986
+ button {
987
+ font-family: var(--elena-button-font);
988
+ color: var(--elena-button-text);
989
+ background: var(--elena-button-bg);
990
+ display: inline-block;
991
+ appearance: none;
992
+ }
993
+
994
+ /* Rest of your component styles */
995
+ button {
996
+ display: inline-flex;
997
+ }
998
+ :scope[variant="primary"] {
999
+ --elena-button-bg: red;
1000
+ }
1001
+ }
1002
+ ```
1003
+
1004
+ The above patterns work great for **Primitive Components** that are self-contained and own and render their own HTML markup.
1005
+
1006
+ ### Elena CSS Encapsulation Pattern
1007
+
1008
+ While the [scoped styles](#writing-scoped-styles) defined earlier prevent the component styles from leaking out, it does not prevent global styles from leaking in. For this, you can use this pattern that does both and then add your own component styles below:
1009
+
1010
+ ```css
1011
+ /* Scope makes sure styles don’t leak out */
1012
+ @scope (elena-button) {
1013
+
1014
+ /* Unset makes sure styles don’t leak in */
1015
+ :scope, *, *::before, *::after {
1016
+ all: unset; /* Or all: initial */
1017
+ }
1018
+
1019
+ /* Rest of your component styles */
1020
+ }
1021
+ ```
1022
+
1023
+ ### Pre-hydration state and styles
1024
+
1025
+ Since **Primitive Components** are self-contained and render their own HTML markup, you may sometimes need access to more than just the initial text content pre-hydration for better SSR support to avoid layout shifts.
1026
+
1027
+ This can be achieved with pseudo elements in CSS by referencing the attributes set on the element itself:
1028
+
1029
+ ```css
1030
+ :scope:not([hydrated])::before {
1031
+ content: attr(label);
1032
+ /* etc */
1033
+ }
1034
+
1035
+ :scope:not([hydrated])::after {
1036
+ content: attr(placeholder);
1037
+ /* etc */
1038
+ }
1039
+ ```
1040
+
1041
+ For more detailed guidelines, see the [Server Side Rendering](#server-side-rendering) section.
1042
+
1043
+ > [!TIP]
1044
+ > You can skip this section entirely for Composite Components, when you plan to [hide components until loaded](#hide-until-loaded), or when the rest of your app renders client side only.
1045
+
1046
+ ### Styling Composite Components
1047
+
1048
+ When styling **Composite Components** which wrap and enhance the HTML composed inside them, you would commonly style the host element and then provide customization with the props set on the component:
1049
+
1050
+ ```css
1051
+ /* Scope makes sure styles don’t leak out */
1052
+ @scope (elena-stack) {
1053
+
1054
+ /* Targets the host element (elena-stack) */
1055
+ :scope {
1056
+ display: flex;
1057
+ justify-content: flex-start;
1058
+ align-items: flex-start;
1059
+ flex-flow: column wrap;
1060
+ flex-direction: column;
1061
+ gap: 0.5rem;
1062
+ }
1063
+
1064
+ /* Attributes provide customization */
1065
+ :scope[direction="row"] {
1066
+ flex-direction: row;
1067
+ }
1068
+ }
1069
+ ```
1070
+
1071
+ Notice above that you don’t have to worry about the pre-hydrated/hydrated states when styling **Composite Components** as all of their HTML lives in the `Light DOM`.
1072
+
1073
+ ### Documenting public CSS properties
1074
+
1075
+ The documentation for the component’s public CSS properties lives in the component itself:
1076
+
1077
+ ```js
1078
+ /**
1079
+ * The description of the component goes here.
1080
+ *
1081
+ * @cssprop [--elena-button-text] - Overrides the default text color.
1082
+ * @cssprop [--elena-button-bg] - Overrides the default background color.
1083
+ * @cssprop [--elena-button-font] - Overrides the default font-family.
1084
+ */
1085
+ export default class Button extends Elena(HTMLElement) { /*...*/ }
1086
+ ```
1087
+
1088
+ > [!TIP]
1089
+ > **`@elenajs/bundler`** transforms the above JSDocs automatically to Custom Elements Manifest which allows you to generate documentation that surfaces the component’s public CSS properties.
1090
+
1091
+ <br/>
1092
+
1093
+ ## Misc
1094
+
1095
+ ### Load event
1096
+
1097
+ Elena web components are self-contained and can be loaded and defined asynchronously. Therefore an element may not be interactive immediately.
1098
+
1099
+ If you set a property on an Elena web component before it has been fully initialized, it will be applied correctly and will use the values once it has finished client side hydration. However, you cannot call a method on an element before the JavaScript has been loaded.
1100
+
1101
+ Most of the time this is not an issue, as you will be calling methods through event handlers. In cases where you want to call a method as soon as possible, for example during a page load, you need to wait for the Elena web component to be defined, using `customElements.whenDefined`:
1102
+
1103
+ ```html
1104
+ <script type="module">
1105
+ const button = document.querySelector("elena-button");
1106
+
1107
+ // It's fine to set props while an Elena Element is loading
1108
+ button.variant = "primary";
1109
+
1110
+ // But if you want to immediately call a method, you should
1111
+ // wait for the Elena Element to be defined
1112
+ await customElements.whenDefined("elena-button");
1113
+ button.click();
1114
+ </script>
1115
+ ```
1116
+
1117
+ ### Hide until loaded
1118
+
1119
+ Sometimes you may want to hide your web components until they’re hydrated and interactive. You can achieve that with this small code snippet from [Scott Jehl](https://scottjehl.com/posts/web-component-self-destruct-css/):
1120
+
1121
+ ```css
1122
+ @keyframes hideElena {
1123
+ 0%,
1124
+ 100% {
1125
+ visibility: hidden;
1126
+ }
1127
+ }
1128
+ :not(:defined) {
1129
+ animation: hideElena 2s;
1130
+ }
1131
+ ```
1132
+
1133
+ > [!TIP]
1134
+ > This CSS snippet will take care that as soon as your elements get defined, the hiding will instantly and automatically unapply. But it will also unapply itself after two seconds no matter what, should the JavaScript take that long to do its thing, or fail to run at all.
1135
+
1136
+ <br/>
1137
+
1138
+ ## Known issues
1139
+
1140
+ ### Browser compatibility
1141
+
1142
+ - Firefox 148 has an open issue regarding CSS `@scope` and `attr[value]` selector that we’ve [documented here](https://codepen.io/arielsalminen/full/raMazZV). This is already fixed in the pre-release build though and that should be out soon.
1143
+
1144
+ ### JavaScript frameworks
1145
+
1146
+ Rules that apply to **Primitive Components** when used with a framework:
1147
+
1148
+ - Never render a framework component _inside_ a Primitive Component (e.g. via `ReactDOM.createRoot(elenaElement)`). Elena calls `replaceChildren()` on render, which would destroy the framework’s fiber tree and cause DOM corruption.
1149
+ - Avoid a JavaScript framework and Elena both mutating the same attribute on a Primitive Component. A framework’s reconciler would overwrite Elena’s changes on next reconcile, triggering many re-renders. Treat framework-controlled props as read-only inputs inside your Elena element’s `render()`:
1150
+
1151
+ ```js
1152
+ // Good: framework passes text, Elena renders it
1153
+ <elena-button text={state.text} />
1154
+
1155
+ render() {
1156
+ // Good: only reads, never writes back
1157
+ return html`<button>${this.text}</button>`;
1158
+ }
1159
+ // Good: Elena communicates back via events, framework updates state
1160
+ <elena-button text={state.text} onclick={e => setState(...)} />
1161
+ ```
1162
+
1163
+ ```js
1164
+ // Bad: Elena writes back to a framework-controlled prop
1165
+ render() {
1166
+ this.setAttribute("label", this.label.toUpperCase()); // ← don't do this
1167
+ }
1168
+ ```
1169
+
1170
+ - You can’t pass dynamic text content as children. Instead use the `text` property, since **Primitive Components** own their internal DOM and frameworks cannot update children after the initial Elena render:
1171
+
1172
+ ```jsx
1173
+ // React
1174
+ <elena-button text={buttonText} />
1175
+
1176
+ // Angular
1177
+ <elena-button [text]="buttonText"></elena-button>
1178
+
1179
+ // Vue
1180
+ <elena-button :text="buttonText"></elena-button>
1181
+ ```
1182
+
1183
+ > [!WARNING]
1184
+ > React 17 does not pass `Array` or `Object` type props or event handlers to web components correctly. Use React 18+ for proper Elena support, or pass all props as string attributes.
1185
+
1186
+ <br/>
1187
+
1188
+ ## Packages
1189
+
1190
+ Elena is a monorepo containing several packages published to npm under the `@elenajs` scope:
1191
+
1192
+ - **[`@elenajs/core`](https://github.com/getelena/elena/tree/main/packages/core)** [![stability-release-candidate](https://img.shields.io/badge/stability-pre--release-48c9b0.svg)](https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md#release-candidate)
1193
+ - **[`@elenajs/cli`](https://github.com/getelena/elena/tree/main/packages/cli)** [![stability-release-candidate](https://img.shields.io/badge/stability-pre--release-48c9b0.svg)](https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md#release-candidate)
1194
+ - **[`@elenajs/bundler`](https://github.com/getelena/elena/tree/main/packages/bundler)** [![stability-beta](https://img.shields.io/badge/stability-beta-33bbff.svg)](https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md#beta)
1195
+ - **[`@elenajs/plugin-cem-define`](https://github.com/getelena/elena/tree/main/packages/plugin-cem-define)** [![stability-beta](https://img.shields.io/badge/stability-beta-33bbff.svg)](https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md#beta)
1196
+ - **[`@elenajs/plugin-cem-tag`](https://github.com/getelena/elena/tree/main/packages/plugin-cem-tag)** [![stability-beta](https://img.shields.io/badge/stability-beta-33bbff.svg)](https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md#beta)
1197
+ - **[`@elenajs/plugin-cem-typescript`](https://github.com/getelena/elena/tree/main/packages/plugin-cem-typescript)** [![stability-beta](https://img.shields.io/badge/stability-beta-33bbff.svg)](https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md#beta)
1198
+ - **[`@elenajs/plugin-rollup-css`](https://github.com/getelena/elena/tree/main/packages/plugin-rollup-css)** [![stability-beta](https://img.shields.io/badge/stability-beta-33bbff.svg)](https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md#beta)
1199
+ - **[`@elenajs/components`](https://github.com/getelena/elena/tree/main/packages/components)** [![stability-alpha](https://img.shields.io/badge/stability-alpha-f4d03f.svg)](https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md#alpha)
1200
+ - **[`@elenajs/ssr`](https://github.com/getelena/elena/tree/main/packages/ssr)** [![stability-experimental](https://img.shields.io/badge/stability-experimental-orange.svg)](https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md#experimental)
1201
+
1202
+ <!-- https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md -->
1203
+
1204
+ <br/>
1205
+
1206
+ ## Development
1207
+
1208
+ ### Commands
1209
+
1210
+ All commands run from the monorepo root (`elena/`):
1211
+
1212
+ ```bash
1213
+ pnpm install # Install dependencies
1214
+ pnpm build # Build all packages
1215
+ pnpm test # Run all tests
1216
+ pnpm lint # Lint with ESLint
1217
+ ```
1218
+
1219
+ Core package commands (from `packages/core/`):
1220
+
1221
+ ```bash
1222
+ pnpm start # Rollup watch
1223
+ pnpm build # Rollup build
1224
+ pnpm test # Vitest with coverage
1225
+ pnpm test:visual # Playwright visual regression tests
1226
+ pnpm test:visual:update # Update visual test baselines
1227
+ pnpm bench # Run performance benchmarks
1228
+ npx vitest run test/props.test.js # Run a single test file
1229
+ ```
1230
+
1231
+ Elements dev server (from `packages/components/`):
1232
+
1233
+ ```bash
1234
+ pnpm start # web-dev-server with live reload
1235
+ ```
1236
+
1237
+ For more details about pull requests, commit conventions and code style, please see [CONTRIBUTING.md](CONTRIBUTING.md).
1238
+
1239
+ <br/>
1240
+
1241
+ ## License
1242
+
1243
+ MIT
1244
+
1245
+ <br/>
1246
+
1247
+ ## Copyright
1248
+
1249
+ Copyright © 2026 [Ariel Salminen](https://arielsalminen.com)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elenajs/core",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Elena is a simple, tiny library for building Progressive Web Components.",
5
5
  "author": "Elena <hi@elenajs.com>",
6
6
  "homepage": "https://elenajs.com/",
@@ -36,5 +36,5 @@
36
36
  "typescript": "5.9.3",
37
37
  "vitest": "4.0.18"
38
38
  },
39
- "gitHead": "81fe2fc78111f605854c07df2001f746231d9dfc"
39
+ "gitHead": "c181af0625f691a95a5cb1ef9adb0d7f2c796eae"
40
40
  }