custom-elements-ts 0.0.16 → 0.1.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 (69) hide show
  1. package/.eslintrc.json +46 -0
  2. package/.github/workflows/ci.yml +49 -0
  3. package/.prettierrc +7 -0
  4. package/LICENSE +20 -0
  5. package/README.md +426 -168
  6. package/demos/counter/counter.element.html +1 -0
  7. package/demos/counter/counter.element.scss +234 -0
  8. package/demos/counter/counter.element.ts +68 -0
  9. package/demos/counter/index.html +205 -0
  10. package/demos/counter/index.ts +1 -0
  11. package/demos/site/code-example/code-example.element.scss +168 -0
  12. package/demos/site/code-example/code-example.element.ts +88 -0
  13. package/demos/site/event-log/event-log.element.scss +179 -0
  14. package/demos/site/event-log/event-log.element.ts +134 -0
  15. package/demos/site/favicon.svg +14 -0
  16. package/demos/site/index.html +346 -0
  17. package/demos/site/index.ts +13 -0
  18. package/demos/site/message/message.element.scss +75 -0
  19. package/demos/site/message/message.element.ts +76 -0
  20. package/demos/site/og-image.png +0 -0
  21. package/demos/site/styles/site.css +1023 -0
  22. package/demos/site/styles/tokens.css +56 -0
  23. package/demos/site/toast/toast.element.scss +110 -0
  24. package/demos/site/toast/toast.element.ts +63 -0
  25. package/demos/todo-dashboard/index.html +141 -0
  26. package/demos/todo-dashboard/index.ts +4 -0
  27. package/demos/todo-dashboard/todo-dashboard.element.scss +1145 -0
  28. package/demos/todo-dashboard/todo-dashboard.element.ts +332 -0
  29. package/demos/todo-dashboard/todo-filters.element.ts +54 -0
  30. package/demos/todo-dashboard/todo-item.element.ts +126 -0
  31. package/demos/todo-dashboard/todo-stats.element.ts +189 -0
  32. package/package.json +73 -29
  33. package/src/custom-element.ts +206 -0
  34. package/{index.d.ts → src/index.ts} +2 -0
  35. package/src/listen.ts +70 -0
  36. package/src/prop.ts +92 -0
  37. package/src/state.ts +129 -0
  38. package/src/template-runtime.ts +435 -0
  39. package/src/toggle.ts +66 -0
  40. package/src/tsconfig.json +24 -0
  41. package/src/util.ts +33 -0
  42. package/src/watch.ts +14 -0
  43. package/tests/basic.spec.ts +70 -0
  44. package/tests/custom-element.spec.ts +77 -0
  45. package/tests/dispatch.spec.ts +52 -0
  46. package/tests/init.spec.ts +94 -0
  47. package/tests/listen.spec.ts +118 -0
  48. package/tests/prop.spec.ts +118 -0
  49. package/tests/templating-runtime.spec.ts +575 -0
  50. package/tests/toggle.spec.ts +92 -0
  51. package/tests/watch.spec.ts +183 -0
  52. package/tools/build.js +119 -0
  53. package/tools/bundle.js +167 -0
  54. package/tools/rollup-config.js +70 -0
  55. package/tools/start.js +188 -0
  56. package/tsconfig.json +38 -0
  57. package/vite.config.mts +30 -0
  58. package/bundles/custom-elements-ts.umd.js +0 -315
  59. package/bundles/custom-elements-ts.umd.js.map +0 -1
  60. package/custom-element.d.ts +0 -12
  61. package/esm2015/custom-elements-ts.js +0 -260
  62. package/esm2015/custom-elements-ts.js.map +0 -1
  63. package/esm5/custom-elements-ts.js +0 -298
  64. package/esm5/custom-elements-ts.js.map +0 -1
  65. package/listen.d.ts +0 -17
  66. package/prop.d.ts +0 -2
  67. package/toggle.d.ts +0 -1
  68. package/util.d.ts +0 -4
  69. package/watch.d.ts +0 -1
package/README.md CHANGED
@@ -1,168 +1,426 @@
1
- # custom-elements-ts
2
-
3
- [![Coverage Status](https://coveralls.io/repos/github/geocine/custom-elements-ts/badge.svg?branch=master)](https://coveralls.io/github/geocine/custom-elements-ts?branch=master)
4
- [![Build Status](https://travis-ci.org/geocine/custom-elements-ts.svg?branch=master)](https://travis-ci.org/geocine/custom-elements-ts)
5
- [![npm version](https://badge.fury.io/js/custom-elements-ts.svg)](https://www.npmjs.com/package/custom-elements-ts)
6
- [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
-
8
-
9
- Create native custom elements using Typescript without using any third party libraries.
10
-
11
- ```
12
- npm install custom-elements-ts
13
- ```
14
-
15
- ## Usage
16
-
17
- ```ts
18
- import { CustomElement } from 'custom-elements-ts';
19
-
20
- @CustomElement({
21
- tag: 'counter-element',
22
- templateUrl: 'counter-element.html',
23
- styleUrl: 'counter-element.scss'
24
- })
25
- export class CounterElement extends HTMLElement {
26
- // code as you would when creating a native HTMLElement
27
- // full source code is at demo/counter
28
- }
29
- ```
30
-
31
- ```html
32
- <!--index.html-->
33
- <counter-element></counter-element>
34
- <script src="counter.umd.js"></script>
35
- ```
36
-
37
- ## Decorators
38
-
39
- | Decorator | Target | Parameters | Description |
40
- |-------------|----------|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
41
- | @Prop() | property | - | custom attribute/properties, reflects primitive properties to attributes |
42
- | @Toggle() | property | - | boolean attribute/properties, it is based on the presence of the attribute but also works with "true" and "false" |
43
- | @Dispatch() | property | (event?) | used to declare a CustomEvent which you could dispatch using the `.emit` method of its type `DispatchEmitter`. The `event` parameter is used to set the name of the `CustomEvent` |
44
- | @Watch() | method | (property) | triggers the method when a `property` is changed |
45
- | @Listen() | method | (event, selector?) | listens to an `event` on the `host` element or on the `selector` if specified |
46
-
47
- ### @Prop()
48
- ```ts
49
- import { CustomElement, Prop } from 'custom-elements-ts';
50
-
51
- @CustomElement({
52
- tag: 'todo-list',
53
- ...
54
- })
55
- export class TodoList extends HTMLElement {
56
- @Prop() color: string;
57
- @Prop() list: TodoItem[];
58
- }
59
- ```
60
- Since `color` is a primitive type of `string` it can be accessed via attributes and properties
61
- ```ts
62
- const element = document.querySelector('todo-list');
63
- // accessing value via attribute
64
- const attrValue = element.getAttribute('color');
65
- // setting value via attribute
66
- element.setAttribute('color', 'red');
67
-
68
- // accessing value via property
69
- const propertyValue = element.color;
70
- // setting via property
71
- element.color = 'red';
72
- ```
73
-
74
- On the other hand `list` is a rich data type (objects or arrays) can only be accessed/set via property
75
-
76
- ### @Toggle()
77
- Toggle attributes work the same way as HTML boolean attributes as defined by [W3C](http://www.w3.org/TR/2008/WD-html5-20080610/semantics.html#boolean) for the most part. We changed a few things to overcome confusion. Check the table below for reference:
78
-
79
- | Markup | `disabled` | Description |
80
- |-------------------------------|------------|----------------------------------------------------------------------|
81
- | `<c-input />` | false | Follows W3C standard |
82
- | `<c-input disabled/>` | true | Follows W3C standard |
83
- | `<c-input disabled="true"/>` | true | Follows W3C standard |
84
- | `<c-input disabled="asd"/>` | false | `false` since `asd` does not evaluate to a valid boolean |
85
- | `<c-input disabled="false"/>` | false | `false` since the boolean `false` converted to a string is `"false"` |
86
- | `<c-input disabled="true"/>` | true | `true` since the boolean `true` converted to a string is `"true"` |
87
-
88
- ### @Dispatch()
89
-
90
- **Creating a custom event**
91
-
92
- ```ts
93
- import { CustomElement, Dispatch, DispatchEmitter } from 'custom-elements-ts';
94
-
95
- ...
96
- export class TodoList extends HTMLElement {
97
- // Creating a CustomEvent
98
- // custom event name will be `on.change`
99
- @Dispatch() onChange: DispatchEmitter;
100
-
101
- // Creating a CustomEvent with custom name `ce.select`
102
- @Dispatch('ce.select') onSelect: DispatchEmitter;
103
- }
104
- ```
105
- **Triggering the custom event** from the example above:
106
-
107
- ```ts
108
- triggerOnChange() {
109
- // adding more data to the event object
110
- this.onChange.emit({detail: 'event changed'});
111
- this.onSelect.emit({detail: 'select triggered'});
112
- }
113
- ```
114
- ### @Watch()
115
-
116
- ```ts
117
- import { CustomElement, Dispatch, Prop } from 'custom-elements-ts';
118
-
119
- ...
120
- export class TodoList extends HTMLElement {
121
- @Prop() color: string;
122
-
123
- @Watch('color')
124
- colorChanged() {
125
- // trigger when color property color changes
126
- // either via property or attribute
127
- }
128
- }
129
- ```
130
-
131
- ### @Listen()
132
-
133
- Listen has parameters `event` and `selector`. `Event` is any valid javascript event. `Selector` is anything that works with [querySelector()](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
134
-
135
- ```ts
136
- import { CustomElement, Dispatch, Prop } from 'custom-elements-ts';
137
-
138
- ...
139
- export class TodoList extends HTMLElement {
140
- @Listen('click')
141
- elementClicked() {
142
- // triggers when the element is clicked
143
- }
144
-
145
- @Listen('click','a')
146
- anchorClicked() {
147
- // triggers when an `a` inside the element is clicked
148
- }
149
- }
150
- ```
151
-
152
- ## Setup
153
-
154
- ### Running the demos
155
-
156
- ```
157
- npm start <element-name>
158
- ```
159
-
160
- ### Building the demo
161
-
162
- ```
163
- npm run build <element-name>
164
- ```
165
- If you want to create a minified bundle
166
- ```
167
- npm run build -- <element-name> --prod
168
- ```
1
+ # custom-elements-ts
2
+
3
+ [![Coverage Status](https://coveralls.io/repos/github/geocine/custom-elements-ts/badge.svg?branch=master)](https://coveralls.io/github/geocine/custom-elements-ts?branch=master)
4
+ ![CI](https://github.com/geocine/custom-elements-ts/actions/workflows/ci.yml/badge.svg)
5
+ [![npm version](https://badge.fury.io/js/custom-elements-ts.svg)](https://www.npmjs.com/package/custom-elements-ts)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ Author native Web Components in TypeScript with a small set of decorators
9
+ (`@CustomElement`, `@Prop`, `@State`, `@Watch`, `@Listen`, `@Dispatch`,
10
+ `@Toggle`) plus a tiny `html` / `render()` runtime. **Zero dependencies.
11
+ Framework-free.**
12
+
13
+ ```
14
+ npm install custom-elements-ts
15
+ ```
16
+
17
+ > **Live demos:** [geocine.github.io/custom-elements-ts](https://geocine.github.io/custom-elements-ts/) — counter, sprint board, install pill, and a live event log, all built with the library.
18
+
19
+ ## Table of contents
20
+
21
+ - [Quick start](#quick-start)
22
+ - [Plain HTML — no `render()` required](#plain-html--no-render-required)
23
+ - [Reactive components with `render()`](#reactive-components-with-render)
24
+ - [Template bindings](#template-bindings)
25
+ - [Decorators](#decorators)
26
+ - [@Prop()](#prop)
27
+ - [@State()](#state)
28
+ - [@Toggle()](#toggle)
29
+ - [@Dispatch()](#dispatch)
30
+ - [@Watch()](#watch)
31
+ - [@Listen()](#listen)
32
+ - [Project layout](#project-layout)
33
+ - [Running the demos](#running-the-demos)
34
+ - [Building](#building)
35
+
36
+ ## Quick start
37
+
38
+ There are **two ways** to author a component, and they compose freely:
39
+
40
+ 1. **Plain HTML, imperative updates.** Declare a `template` (or
41
+ `templateUrl`) and update the DOM yourself in `connectedCallback`,
42
+ `@Watch()` handlers, or `@Listen()` handlers. No `render()`. No
43
+ reactive runtime. **Use this when the DOM is mostly static**
44
+ buttons, badges, panels, copy-to-clipboard pills, and so on.
45
+ 2. **Reactive `render()` with the `html` helper.** Define `render()` and
46
+ the runtime re-renders for you on `@Prop()` / `@State()` / `@Toggle()`
47
+ changes. Use this for stateful components like dashboards, counters,
48
+ forms, and lists.
49
+
50
+ `render()` is **optional** — components without it pay zero runtime
51
+ cost beyond the decorators themselves.
52
+
53
+ ### Plain HTML — no `render()` required
54
+
55
+ A small toast-firing "click to copy" pill, written entirely with a
56
+ static template and imperative DOM. This is exactly the pattern used by
57
+ `<cts-message>` on the [showcase page](https://geocine.github.io/custom-elements-ts/):
58
+
59
+ ```ts
60
+ import {
61
+ CustomElement,
62
+ Prop,
63
+ Listen,
64
+ Dispatch,
65
+ DispatchEmitter,
66
+ } from 'custom-elements-ts';
67
+
68
+ @CustomElement({
69
+ tag: 'cts-message',
70
+ template: `
71
+ <div class="row" role="button" tabindex="0">
72
+ <span class="prompt">$</span>
73
+ <code class="cmd"></code>
74
+ </div>
75
+ `,
76
+ styleUrl: './message.element.scss',
77
+ })
78
+ export class MessageElement extends HTMLElement {
79
+ @Prop() message!: string;
80
+
81
+ // Bubbling, composed CustomEvent any ancestor can listen for it
82
+ // (e.g. a <cts-toast> at the document root).
83
+ @Dispatch('cts:toast') toast!: DispatchEmitter;
84
+
85
+ connectedCallback() {
86
+ // Imperative DOM update no render() needed.
87
+ this.shadowRoot!.querySelector('.cmd')!.textContent = this.message;
88
+ }
89
+
90
+ @Listen('click')
91
+ async handleClick() {
92
+ await navigator.clipboard.writeText(this.message);
93
+ this.toast.emit({
94
+ bubbles: true,
95
+ composed: true,
96
+ detail: { title: 'Copied to clipboard', message: this.message },
97
+ });
98
+ }
99
+ }
100
+ ```
101
+
102
+ ```html
103
+ <!-- Drop it anywhere — React, Vue, Svelte, plain HTML — it just works -->
104
+ <cts-message message="npm install custom-elements-ts"></cts-message>
105
+ <script src="message.umd.js"></script>
106
+ ```
107
+
108
+ You can also keep markup in its own file with `templateUrl` and
109
+ `styleUrl`, exactly as you would with any other framework:
110
+
111
+ ```ts
112
+ @CustomElement({
113
+ tag: 'counter-element',
114
+ templateUrl: './counter-element.html',
115
+ styleUrl: './counter-element.scss',
116
+ })
117
+ export class CounterElement extends HTMLElement {
118
+ // Wire up DOM manually in connectedCallback / @Watch / @Listen.
119
+ }
120
+ ```
121
+
122
+ ### Reactive components with `render()`
123
+
124
+ Add a `render()` method that returns an `html` template literal and the
125
+ runtime takes care of efficient DOM updates whenever any
126
+ `@Prop()` / `@State()` / `@Toggle()` value changes:
127
+
128
+ ```ts
129
+ import { CustomElement, State, html } from 'custom-elements-ts';
130
+
131
+ @CustomElement({ tag: 'cts-counter' })
132
+ export class CounterElement extends HTMLElement {
133
+ @State() count = 0;
134
+
135
+ render() {
136
+ return html`<button @click=${this.increment}>Count: ${this.count}</button>`;
137
+ }
138
+
139
+ private increment() {
140
+ this.count++;
141
+ }
142
+ }
143
+ ```
144
+
145
+ `render()` output is mounted into the shadow root by default. Pass
146
+ `shadow: false` to render into the host element instead.
147
+
148
+ ### Template bindings
149
+
150
+ When you do opt into `render()`, the `html` helper supports the common
151
+ binding forms used by render-based components:
152
+
153
+ ```ts
154
+ html`<p>${this.label}</p>`;
155
+ html`<p>${() => this.label}</p>`;
156
+ html`<button @click=${this.handleClick}></button>`;
157
+ html`<input .value=${this.value} />`;
158
+ html`<div title=${this.title}></div>`;
159
+ html`<ul>
160
+ ${this.items.map((item) => html`<li>${item.label}</li>`)}
161
+ </ul>`;
162
+ ```
163
+
164
+ Attribute bindings remove the attribute when the value is `false`,
165
+ `null`, or `undefined`. Event bindings replace old listeners when a
166
+ render supplies a new handler and are cleaned up automatically when the
167
+ rendered template is disposed.
168
+
169
+ ## Decorators
170
+
171
+ | Decorator | Target | Parameters | Description |
172
+ | ----------- | -------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
173
+ | @Prop() | property | - | custom attribute/properties; reflects primitive values (string, number, boolean) to attributes |
174
+ | @State() | property | - | private reactive state for render-based components; not reflected to attributes |
175
+ | @Toggle() | property | - | boolean attribute/properties based on the presence of the attribute; also accepts `"true"` and `"false"` |
176
+ | @Dispatch() | property | (event?) | declares a `CustomEvent` you can fire via the `.emit` method of its `DispatchEmitter` type. The `event` parameter sets the `CustomEvent` name |
177
+ | @Watch() | method | (property) | runs the method when `property` changes |
178
+ | @Listen() | method | (event, selector?) | listens for `event` on the host (or on `selector` inside the shadow tree) |
179
+
180
+ ### @Prop()
181
+
182
+ ```ts
183
+ import { CustomElement, Prop } from 'custom-elements-ts';
184
+
185
+ @CustomElement({
186
+ tag: 'todo-list',
187
+ ...
188
+ })
189
+ export class TodoList extends HTMLElement {
190
+ @Prop() color: string;
191
+ @Prop() list: TodoItem[];
192
+ }
193
+ ```
194
+
195
+ Since `color` is a primitive type of `string` it can be accessed via
196
+ attributes and properties:
197
+
198
+ ```ts
199
+ const element = document.querySelector('todo-list');
200
+ // accessing value via attribute
201
+ const attrValue = element.getAttribute('color');
202
+ // setting value via attribute
203
+ element.setAttribute('color', 'red');
204
+
205
+ // accessing value via property
206
+ const propertyValue = element.color;
207
+ // setting via property
208
+ element.color = 'red';
209
+ ```
210
+
211
+ `list` is a rich data type (objects or arrays) and functions/classes can
212
+ only be accessed/set via property — they are not reflected as
213
+ attributes:
214
+
215
+ ```ts
216
+ // Functions and classes are not reflected to attributes
217
+ @Prop() onChange: (detail: any) => void;
218
+ @Prop() itemConstructor: { new(...args: any[]): any };
219
+
220
+ element.onChange = () => {};
221
+ // not reflected as attribute
222
+ console.log(element.getAttribute('on-change')); // null
223
+
224
+ class Foo {}
225
+ element.itemConstructor = Foo;
226
+ // not reflected as attribute
227
+ console.log(element.getAttribute('item-ctor')); // null
228
+ ```
229
+
230
+ Render-based components update after a real `@Prop()` value change.
231
+ Multiple prop and state changes inside the same synchronous turn are
232
+ batched into one render.
233
+
234
+ ### @State()
235
+
236
+ ```ts
237
+ import { CustomElement, State, Watch, html } from 'custom-elements-ts';
238
+
239
+ @CustomElement({ tag: 'profile-card' })
240
+ export class ProfileCard extends HTMLElement {
241
+ @State() user = { name: 'Ada' };
242
+ @State() items = [{ label: 'One' }];
243
+
244
+ @Watch('user')
245
+ userChanged(value: { old: unknown; new: unknown }) {
246
+ console.log(value.new);
247
+ }
248
+
249
+ render() {
250
+ return html`
251
+ <strong>${this.user.name}</strong>
252
+ <ul>
253
+ ${this.items.map((item) => html`<li>${item.label}</li>`)}
254
+ </ul>
255
+ `;
256
+ }
257
+ }
258
+ ```
259
+
260
+ State is internal to the element: it is not reflected to attributes and
261
+ is not included in `observedAttributes`. Plain objects and arrays
262
+ assigned to state are deeply proxied, so nested mutations such as
263
+ `this.user.name = 'Grace'`, `this.items.push(...)`, and
264
+ `this.items[0].label = 'Updated'` schedule a render.
265
+
266
+ Only plain objects and arrays are proxied. Functions, class
267
+ constructors, DOM nodes, `Date`, `Map`, `Set`, `WeakMap`, and `WeakSet`
268
+ are left as-is — reassign those values to trigger a render.
269
+
270
+ ### @Toggle()
271
+
272
+ Toggle attributes work the same way as HTML boolean attributes as
273
+ defined by [W3C](http://www.w3.org/TR/2008/WD-html5-20080610/semantics.html#boolean)
274
+ for the most part. We changed a few things to overcome confusion. Check
275
+ the table below for reference:
276
+
277
+ | Markup | `disabled` | Description |
278
+ | ----------------------------- | ---------- | -------------------------------------------------------------------- |
279
+ | `<c-input />` | false | Follows W3C standard |
280
+ | `<c-input disabled/>` | true | Follows W3C standard |
281
+ | `<c-input disabled="true"/>` | true | Follows W3C standard |
282
+ | `<c-input disabled="asd"/>` | false | `false` since `asd` does not evaluate to a valid boolean |
283
+ | `<c-input disabled="false"/>` | false | `false` since the boolean `false` converted to a string is `"false"` |
284
+ | `<c-input disabled="true"/>` | true | `true` since the boolean `true` converted to a string is `"true"` |
285
+
286
+ ### @Dispatch()
287
+
288
+ **Creating a custom event**
289
+
290
+ ```ts
291
+ import { CustomElement, Dispatch, DispatchEmitter } from 'custom-elements-ts';
292
+
293
+ ...
294
+ export class TodoList extends HTMLElement {
295
+ // Creating a CustomEvent
296
+ // custom event name will be `on.change`
297
+ @Dispatch() onChange: DispatchEmitter;
298
+
299
+ // Creating a CustomEvent with custom name `ce.select`
300
+ @Dispatch('ce.select') onSelect: DispatchEmitter;
301
+ }
302
+ ```
303
+
304
+ **Triggering the custom event** from the example above:
305
+
306
+ ```ts
307
+ triggerOnChange() {
308
+ // adding more data to the event object
309
+ this.onChange.emit({ detail: 'event changed' });
310
+ this.onSelect.emit({ detail: 'select triggered' });
311
+ }
312
+ ```
313
+
314
+ For events that need to cross the shadow boundary (e.g. so a parent or
315
+ the document can listen) opt into bubbling and composed delivery on the
316
+ `emit()` call:
317
+
318
+ ```ts
319
+ this.onChange.emit({
320
+ bubbles: true,
321
+ composed: true,
322
+ detail: { count: this.count },
323
+ });
324
+ ```
325
+
326
+ ### @Watch()
327
+
328
+ ```ts
329
+ import { CustomElement, Prop, Watch } from 'custom-elements-ts';
330
+
331
+ ...
332
+ export class TodoList extends HTMLElement {
333
+ @Prop() color: string;
334
+
335
+ @Watch('color')
336
+ colorChanged() {
337
+ // trigger when color property color changes
338
+ // either via property or attribute
339
+ }
340
+ }
341
+ ```
342
+
343
+ ### @Listen()
344
+
345
+ `@Listen()` takes an `event` and an optional `selector`. `event` is any
346
+ valid JavaScript event. `selector` is anything that works with
347
+ [`querySelector()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector).
348
+
349
+ ```ts
350
+ import { CustomElement, Listen } from 'custom-elements-ts';
351
+
352
+ ...
353
+ export class TodoList extends HTMLElement {
354
+ @Listen('click')
355
+ elementClicked() {
356
+ // triggers when the element is clicked
357
+ }
358
+
359
+ @Listen('click', 'a')
360
+ anchorClicked() {
361
+ // triggers when an `a` inside the element is clicked
362
+ }
363
+ }
364
+ ```
365
+
366
+ ## Project layout
367
+
368
+ ```
369
+ src/ # the library — decorators + html/render runtime
370
+ demos/
371
+ counter/ # @State() + render() — single counter card
372
+ todo-dashboard/ # composed elements: stats, filters, items, parent
373
+ site/ # the showcase landing page that hosts every live demo
374
+ tests/ # vitest specs for the runtime + decorators
375
+ tools/ # build / start / bundle scripts
376
+ ```
377
+
378
+ The site demo (`demos/site`) imports the counter and the todo-dashboard
379
+ elements from sibling demo folders, so the showcase page on
380
+ `localhost:3000` runs the **real** components — not screenshots — and
381
+ includes a `<cts-event-log>` panel that subscribes to their bubbling
382
+ `CustomEvent`s in real time.
383
+
384
+ ## Running the demos
385
+
386
+ ```
387
+ npm start <element-name>
388
+ ```
389
+
390
+ | Element | Highlights |
391
+ | ---------------- | ---------------------------------------------------------------- |
392
+ | `site` | Showcase landing page (hero, code preview, live demos, OG graph) |
393
+ | `counter` | `@State()` + `@Watch()` + `@Dispatch()` on a single card |
394
+ | `todo-dashboard` | Parent / child composition with deeply proxied state |
395
+
396
+ ```
397
+ npm start site
398
+ npm start counter
399
+ npm start todo-dashboard
400
+ ```
401
+
402
+ The dev server runs on `http://localhost:3000` and live-reloads on
403
+ TypeScript / SCSS / HTML changes.
404
+
405
+ ## Building
406
+
407
+ ### Building a demo
408
+
409
+ ```
410
+ npm run build <element-name>
411
+ ```
412
+
413
+ For a minified bundle:
414
+
415
+ ```
416
+ npm run build -- <element-name> --prod
417
+ ```
418
+
419
+ ### Building the library (publish artifacts)
420
+
421
+ Builds the library from `src/index.ts` into `dist/` (UMD + ESM builds
422
+ with typings):
423
+
424
+ ```
425
+ npm run bundle
426
+ ```
@@ -0,0 +1 @@
1
+ <button id="count"></button>