custom-elements-ts 0.0.17 → 0.2.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 (82) hide show
  1. package/.eslintrc.json +47 -0
  2. package/.github/workflows/ci.yml +49 -0
  3. package/.prettierrc +7 -0
  4. package/LICENSE +20 -0
  5. package/README.md +279 -37
  6. package/assets/readme-header.png +0 -0
  7. package/assets/social-preview.jpg +0 -0
  8. package/demos/counter/counter.element.html +1 -0
  9. package/demos/counter/counter.element.scss +234 -0
  10. package/demos/counter/counter.element.ts +68 -0
  11. package/demos/counter/index.html +205 -0
  12. package/demos/counter/index.ts +1 -0
  13. package/demos/site/code-example/code-example.element.scss +168 -0
  14. package/demos/site/code-example/code-example.element.ts +88 -0
  15. package/demos/site/event-log/event-log.element.scss +179 -0
  16. package/demos/site/event-log/event-log.element.ts +123 -0
  17. package/demos/site/favicon.svg +14 -0
  18. package/demos/site/index.html +368 -0
  19. package/demos/site/index.ts +19 -0
  20. package/demos/site/llms.txt +53 -0
  21. package/demos/site/message/message.element.scss +75 -0
  22. package/demos/site/message/message.element.ts +76 -0
  23. package/demos/site/og-image.png +0 -0
  24. package/demos/site/scroll-restoration.ts +28 -0
  25. package/demos/site/source-toggle/source-toggle.ts +88 -0
  26. package/demos/site/source-viewer/source-viewer.element.scss +292 -0
  27. package/demos/site/source-viewer/source-viewer.element.ts +138 -0
  28. package/demos/site/source-viewer/sources.generated.ts +11 -0
  29. package/demos/site/styles/site.css +1135 -0
  30. package/demos/site/styles/tokens.css +56 -0
  31. package/demos/site/toast/toast.element.scss +110 -0
  32. package/demos/site/toast/toast.element.ts +63 -0
  33. package/demos/todo-dashboard/index.html +141 -0
  34. package/demos/todo-dashboard/index.ts +4 -0
  35. package/demos/todo-dashboard/todo-dashboard.element.scss +1150 -0
  36. package/demos/todo-dashboard/todo-dashboard.element.ts +333 -0
  37. package/demos/todo-dashboard/todo-filters.element.ts +54 -0
  38. package/demos/todo-dashboard/todo-item.element.ts +127 -0
  39. package/demos/todo-dashboard/todo-stats.element.ts +189 -0
  40. package/package.json +73 -24
  41. package/src/custom-element.ts +206 -0
  42. package/src/index.ts +8 -0
  43. package/src/listen.ts +70 -0
  44. package/src/prop.ts +92 -0
  45. package/src/signal.ts +66 -0
  46. package/src/state.ts +141 -0
  47. package/src/template-runtime.ts +783 -0
  48. package/src/toggle.ts +66 -0
  49. package/src/tsconfig.json +24 -0
  50. package/src/util.ts +33 -0
  51. package/src/watch.ts +14 -0
  52. package/tests/basic.spec.ts +70 -0
  53. package/tests/custom-element.spec.ts +77 -0
  54. package/tests/dispatch.spec.ts +52 -0
  55. package/tests/init.spec.ts +94 -0
  56. package/tests/listen.spec.ts +118 -0
  57. package/tests/map-shallow-state.spec.ts +184 -0
  58. package/tests/prop.spec.ts +118 -0
  59. package/tests/signal.spec.ts +117 -0
  60. package/tests/templating-runtime.spec.ts +575 -0
  61. package/tests/toggle.spec.ts +92 -0
  62. package/tests/watch.spec.ts +183 -0
  63. package/tools/build.js +128 -0
  64. package/tools/bundle.js +167 -0
  65. package/tools/generate-sources.js +76 -0
  66. package/tools/rollup-config.js +70 -0
  67. package/tools/start.js +194 -0
  68. package/tsconfig.json +38 -0
  69. package/vite.config.mts +30 -0
  70. package/bundles/custom-elements-ts.umd.js +0 -359
  71. package/bundles/custom-elements-ts.umd.js.map +0 -1
  72. package/esm2015/custom-elements-ts.js +0 -283
  73. package/esm2015/custom-elements-ts.js.map +0 -1
  74. package/esm5/custom-element.d.ts +0 -12
  75. package/esm5/custom-elements-ts.js +0 -344
  76. package/esm5/custom-elements-ts.js.map +0 -1
  77. package/esm5/index.d.ts +0 -5
  78. package/esm5/listen.d.ts +0 -17
  79. package/esm5/prop.d.ts +0 -2
  80. package/esm5/toggle.d.ts +0 -1
  81. package/esm5/util.d.ts +0 -4
  82. package/esm5/watch.d.ts +0 -1
@@ -0,0 +1,368 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta name="theme-color" content="#0a0c10">
7
+ <meta name="color-scheme" content="dark">
8
+
9
+ <title>custom-elements-ts — native Web Components in TypeScript</title>
10
+ <meta name="description" content="Author native Web Components in TypeScript with a tiny set of decorators — @CustomElement, @Prop, @State, @Watch, @Listen, @Dispatch — and a 3 kB runtime. Zero dependencies. Framework-free.">
11
+
12
+ <!-- Open Graph / Facebook -->
13
+ <meta property="og:type" content="website">
14
+ <meta property="og:site_name" content="custom-elements-ts">
15
+ <meta property="og:url" content="https://geocine.github.io/custom-elements-ts/">
16
+ <meta property="og:title" content="custom-elements-ts — Native Web Components in TypeScript">
17
+ <meta property="og:description" content="A 3 kB decorator-based runtime for authoring standards-compliant custom elements in TypeScript. Zero dependencies. Framework-free.">
18
+ <meta property="og:image" content="https://geocine.github.io/custom-elements-ts/og-image.png">
19
+ <meta property="og:image:secure_url" content="https://geocine.github.io/custom-elements-ts/og-image.png">
20
+ <meta property="og:image:type" content="image/png">
21
+ <meta property="og:image:width" content="1200">
22
+ <meta property="og:image:height" content="630">
23
+ <meta property="og:image:alt" content="custom-elements-ts — Native Web Components, written in TypeScript. Sample code window showing a @CustomElement decorator.">
24
+
25
+ <!-- Twitter / X -->
26
+ <meta name="twitter:card" content="summary_large_image">
27
+ <meta name="twitter:title" content="custom-elements-ts — Native Web Components in TypeScript">
28
+ <meta name="twitter:description" content="A 3 kB decorator-based runtime for authoring standards-compliant custom elements in TypeScript. Zero dependencies. Framework-free.">
29
+ <meta name="twitter:image" content="https://geocine.github.io/custom-elements-ts/og-image.png">
30
+ <meta name="twitter:image:alt" content="custom-elements-ts — Native Web Components, written in TypeScript.">
31
+
32
+ <link rel="canonical" href="https://geocine.github.io/custom-elements-ts/">
33
+ <link rel="icon" type="image/svg+xml" href="favicon.svg">
34
+
35
+ <link rel="preconnect" href="https://fonts.googleapis.com">
36
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
37
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500;600&display=swap">
38
+ <link rel="stylesheet" href="styles/site.css">
39
+ </head>
40
+ <body>
41
+ <a class="skip-link" href="#main">Skip to content</a>
42
+
43
+ <!-- ─── Top navigation ──────────────────────────────────────── -->
44
+ <header class="nav">
45
+ <div class="container nav__inner">
46
+ <a class="brand" href="#">
47
+ <span class="brand__mark" aria-hidden="true">
48
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round">
49
+ <path d="M8.5 7.5 4 12l4.5 4.5"/>
50
+ <path d="M15.5 7.5 20 12l-4.5 4.5"/>
51
+ <path d="M14 5l-4 14"/>
52
+ </svg>
53
+ </span>
54
+ <span class="brand__name">custom-elements<span class="dim">-ts</span></span>
55
+ </a>
56
+
57
+ <nav class="nav__links" aria-label="Primary">
58
+ <a class="nav__link" href="#showcase">Example</a>
59
+ <a class="nav__link" href="#live">Live demos</a>
60
+ <a class="nav__link" href="#api">Decorators</a>
61
+ <a class="nav__link" href="https://github.com/geocine/custom-elements-ts#readme">Docs</a>
62
+ <a class="nav__link" href="https://www.npmjs.com/package/custom-elements-ts">npm</a>
63
+ </nav>
64
+
65
+ <a class="nav__cta" href="https://github.com/geocine/custom-elements-ts" aria-label="View source on GitHub">
66
+ <svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8a8 8 0 0 0 5.47 7.59c.4.07.55-.17.55-.38v-1.33c-2.23.49-2.7-1.07-2.7-1.07-.36-.92-.89-1.17-.89-1.17-.73-.5.05-.49.05-.49.81.06 1.24.83 1.24.83.72 1.23 1.88.88 2.34.67.07-.52.28-.88.51-1.08-1.78-.2-3.65-.89-3.65-3.96 0-.87.31-1.59.83-2.15-.08-.2-.36-1.02.08-2.13 0 0 .67-.21 2.2.82a7.6 7.6 0 0 1 4 0c1.53-1.04 2.2-.82 2.2-.82.44 1.11.16 1.93.08 2.13.52.56.83 1.28.83 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48v2.19c0 .21.15.46.55.38A8 8 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
67
+ <span>GitHub</span>
68
+ </a>
69
+ </div>
70
+ </header>
71
+
72
+ <main id="main">
73
+
74
+ <!-- ─── Hero ────────────────────────────────────────────── -->
75
+ <section class="hero">
76
+ <div class="container">
77
+ <div class="hero__pill" role="status">
78
+ <span class="badge">v0.2.0</span>
79
+ <span>TypeScript&nbsp;5</span>
80
+ <span class="sep">·</span>
81
+ <span>MIT</span>
82
+ <span class="sep">·</span>
83
+ <span>0 deps</span>
84
+ </div>
85
+
86
+ <h1 class="hero__title">
87
+ Native Web Components, written in <em class="em">TypeScript</em>.
88
+ </h1>
89
+
90
+ <p class="hero__lede">
91
+ <strong>custom-elements-ts</strong> is a tiny set of decorators —
92
+ <code>@CustomElement</code>, <code>@Prop</code>, <code>@State</code>,
93
+ <code>@Watch</code>, <code>@Listen</code>, <code>@Dispatch</code>,
94
+ <code>@Toggle</code> — plus a 3&nbsp;kB <code>html</code>/<code>render()</code>
95
+ runtime that lets you author standards-compliant custom elements
96
+ without a framework.
97
+ </p>
98
+
99
+ <div class="hero__actions">
100
+ <a class="btn btn--primary" href="#live">
101
+ See it in action
102
+ <svg class="arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
103
+ </a>
104
+ <a class="btn btn--ghost" href="https://github.com/geocine/custom-elements-ts">
105
+ <svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" width="14" height="14"><path d="M8 0C3.58 0 0 3.58 0 8a8 8 0 0 0 5.47 7.59c.4.07.55-.17.55-.38v-1.33c-2.23.49-2.7-1.07-2.7-1.07-.36-.92-.89-1.17-.89-1.17-.73-.5.05-.49.05-.49.81.06 1.24.83 1.24.83.72 1.23 1.88.88 2.34.67.07-.52.28-.88.51-1.08-1.78-.2-3.65-.89-3.65-3.96 0-.87.31-1.59.83-2.15-.08-.2-.36-1.02.08-2.13 0 0 .67-.21 2.2.82a7.6 7.6 0 0 1 4 0c1.53-1.04 2.2-.82 2.2-.82.44 1.11.16 1.93.08 2.13.52.56.83 1.28.83 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48v2.19c0 .21.15.46.55.38A8 8 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
106
+ Source on GitHub
107
+ </a>
108
+ </div>
109
+
110
+ <div class="hero__meta">
111
+ <span><span class="ok">●</span>&nbsp; Works with any framework — or none</span>
112
+ <span><span class="ok">●</span>&nbsp; ~3 kB min+gzip runtime</span>
113
+ <span><span class="ok">●</span>&nbsp; Shadow DOM &amp; light DOM</span>
114
+ </div>
115
+ </div>
116
+ </section>
117
+
118
+ <!-- ─── Showcase: code preview + install ────────────────── -->
119
+ <section class="showcase" id="showcase">
120
+ <div class="container">
121
+ <div class="showcase__grid">
122
+
123
+ <article class="window" aria-label="message.element.ts source">
124
+ <header class="window__bar">
125
+ <span class="window__dots" aria-hidden="true">
126
+ <span></span><span></span><span></span>
127
+ </span>
128
+ <span class="window__title">
129
+ message.<span class="accent">element.ts</span>
130
+ </span>
131
+ </header>
132
+ <div class="window__body">
133
+ <cts-code-example></cts-code-example>
134
+ </div>
135
+ </article>
136
+
137
+ <aside class="side-card" aria-label="Install custom-elements-ts">
138
+ <div class="side-card__head">
139
+ <div class="side-card__kicker">Install · live</div>
140
+ <h2 class="side-card__title">Click the pill — see the source.</h2>
141
+ <p class="side-card__sub">
142
+ The component on the left <em>is</em> the component below.
143
+ Click to copy, watch <code>cts:toast</code> fire.
144
+ </p>
145
+ </div>
146
+
147
+ <cts-message message="npm install custom-elements-ts"></cts-message>
148
+
149
+ <ul class="checks">
150
+ <li>
151
+ <span class="check" aria-hidden="true">
152
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
153
+ </span>
154
+ <span><strong>Zero dependencies.</strong> Nothing hides under the hood.</span>
155
+ </li>
156
+ <li>
157
+ <span class="check" aria-hidden="true">
158
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
159
+ </span>
160
+ <span><strong>Framework-free.</strong> Drop the same element into React, Vue, Svelte, or plain HTML.</span>
161
+ </li>
162
+ <li>
163
+ <span class="check" aria-hidden="true">
164
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
165
+ </span>
166
+ <span><strong>Tiny surface area.</strong> Seven decorators is the whole API.</span>
167
+ </li>
168
+ </ul>
169
+
170
+ <div class="side-card__foot">
171
+ <a href="https://github.com/geocine/custom-elements-ts#readme">
172
+ Read the full guide
173
+ <svg class="arrow" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
174
+ </a>
175
+ </div>
176
+ </aside>
177
+
178
+ </div>
179
+ </div>
180
+ </section>
181
+
182
+ <!-- ─── Live demos ─────────────────────────────────────── -->
183
+ <section class="live" id="live">
184
+ <div class="container">
185
+
186
+ <div class="live__head">
187
+ <div>
188
+ <span class="eyebrow"><span class="dot"></span> Live in your browser</span>
189
+ <h2 class="live__title">Two demos. Real components. Real events.</h2>
190
+ </div>
191
+ <p class="live__lede">
192
+ Each demo below is a custom element built with the same library you'd
193
+ <code>npm install</code>. The event log on the side is itself a
194
+ <code>render()</code> + <code>@State()</code> component — it subscribes
195
+ to the bubbling, composed <code>CustomEvent</code>s the demo dispatches.
196
+ </p>
197
+ </div>
198
+
199
+ <!-- Counter playground -->
200
+ <article class="playground playground--counter" aria-labelledby="demo-counter-title">
201
+ <header class="playground__head">
202
+ <div>
203
+ <span class="playground__kicker">Demo · 01</span>
204
+ <h3 id="demo-counter-title" class="playground__title">Counter — <code>@State()</code> + <code>@Dispatch()</code></h3>
205
+ <p class="playground__sub">
206
+ A single-button card that owns an internal count, re-renders via the
207
+ <code>html</code> tagged template, and fires <code>counter.change</code>
208
+ whenever its <code>@State()</code> changes.
209
+ </p>
210
+ </div>
211
+ <div class="playground__actions">
212
+ <button class="playground__btn" type="button"
213
+ data-source-toggle data-source-target="src-counter"
214
+ aria-controls="src-counter" aria-expanded="false">
215
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" width="14" height="14"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
216
+ <span>Source</span>
217
+ <svg class="playground__btn-caret" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" width="12" height="12"><polyline points="6 9 12 15 18 9"/></svg>
218
+ </button>
219
+ <a class="playground__btn playground__btn--ghost"
220
+ href="https://github.com/geocine/custom-elements-ts/blob/master/demos/counter/counter.element.ts"
221
+ target="_blank" rel="noopener" aria-label="View counter source on GitHub">
222
+ <svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" width="13" height="13"><path d="M8 0C3.58 0 0 3.58 0 8a8 8 0 0 0 5.47 7.59c.4.07.55-.17.55-.38v-1.33c-2.23.49-2.7-1.07-2.7-1.07-.36-.92-.89-1.17-.89-1.17-.73-.5.05-.49.05-.49.81.06 1.24.83 1.24.83.72 1.23 1.88.88 2.34.67.07-.52.28-.88.51-1.08-1.78-.2-3.65-.89-3.65-3.96 0-.87.31-1.59.83-2.15-.08-.2-.36-1.02.08-2.13 0 0 .67-.21 2.2.82a7.6 7.6 0 0 1 4 0c1.53-1.04 2.2-.82 2.2-.82.44 1.11.16 1.93.08 2.13.52.56.83 1.28.83 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48v2.19c0 .21.15.46.55.38A8 8 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
223
+ <span>GitHub</span>
224
+ </a>
225
+ </div>
226
+ </header>
227
+
228
+ <div class="playground__body playground__body--split">
229
+ <div class="stage stage--counter">
230
+ <cts-counter></cts-counter>
231
+ <p class="stage__caption">
232
+ Click the card. <code>@State() count</code> increments and
233
+ re-renders the template; <code>@Watch('count')</code> emits
234
+ the event picked up on the right.
235
+ </p>
236
+ </div>
237
+
238
+ <cts-event-log
239
+ class="playground__log"
240
+ label="counter.change"
241
+ events="counter.change"
242
+ empty="Click the counter to dispatch counter.change events."
243
+ ></cts-event-log>
244
+ </div>
245
+
246
+ <cts-source-viewer id="src-counter" slug="counter"></cts-source-viewer>
247
+ </article>
248
+
249
+ <!-- Todo dashboard playground -->
250
+ <article class="playground playground--todo" aria-labelledby="demo-todo-title">
251
+ <header class="playground__head">
252
+ <div>
253
+ <span class="playground__kicker">Demo · 02</span>
254
+ <h3 id="demo-todo-title" class="playground__title">Sprint board — composition, deep state, custom events</h3>
255
+ <p class="playground__sub">
256
+ Four cooperating elements: a parent dashboard that owns the source
257
+ of truth, plus child stats, filters, and item rows that talk back
258
+ through bubbling, composed events. Mutate a nested array — the
259
+ parent re-renders.
260
+ </p>
261
+ </div>
262
+ <div class="playground__actions">
263
+ <button class="playground__btn" type="button"
264
+ data-source-toggle data-source-target="src-todo"
265
+ aria-controls="src-todo" aria-expanded="false">
266
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" width="14" height="14"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
267
+ <span>Source</span>
268
+ <svg class="playground__btn-caret" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" width="12" height="12"><polyline points="6 9 12 15 18 9"/></svg>
269
+ </button>
270
+ <a class="playground__btn playground__btn--ghost"
271
+ href="https://github.com/geocine/custom-elements-ts/tree/master/demos/todo-dashboard"
272
+ target="_blank" rel="noopener" aria-label="View todo dashboard source on GitHub">
273
+ <svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" width="13" height="13"><path d="M8 0C3.58 0 0 3.58 0 8a8 8 0 0 0 5.47 7.59c.4.07.55-.17.55-.38v-1.33c-2.23.49-2.7-1.07-2.7-1.07-.36-.92-.89-1.17-.89-1.17-.73-.5.05-.49.05-.49.81.06 1.24.83 1.24.83.72 1.23 1.88.88 2.34.67.07-.52.28-.88.51-1.08-1.78-.2-3.65-.89-3.65-3.96 0-.87.31-1.59.83-2.15-.08-.2-.36-1.02.08-2.13 0 0 .67-.21 2.2.82a7.6 7.6 0 0 1 4 0c1.53-1.04 2.2-.82 2.2-.82.44 1.11.16 1.93.08 2.13.52.56.83 1.28.83 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48v2.19c0 .21.15.46.55.38A8 8 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
274
+ <span>GitHub</span>
275
+ </a>
276
+ </div>
277
+ </header>
278
+
279
+ <div class="playground__body playground__body--split">
280
+ <div class="stage stage--todo">
281
+ <cts-todo-dashboard heading="Sprint board" compact></cts-todo-dashboard>
282
+ </div>
283
+
284
+ <cts-event-log
285
+ class="playground__log"
286
+ label="todo events"
287
+ events="todo.change,todo.toggle,todo.labelchange,todo.remove,filter.change,goal.change"
288
+ empty="Add, complete, edit, or remove a task to see events bubble up here."
289
+ ></cts-event-log>
290
+ </div>
291
+
292
+ <cts-source-viewer id="src-todo" slug="todo-dashboard"></cts-source-viewer>
293
+ </article>
294
+ </div>
295
+ </section>
296
+
297
+ <!-- ─── API at-a-glance ─────────────────────────────────── -->
298
+ <section class="api" id="api">
299
+ <div class="container">
300
+ <div class="api__head">
301
+ <div>
302
+ <span class="eyebrow"><span class="dot"></span> The API</span>
303
+ <h2 class="api__title">Seven decorators. That's the whole library.</h2>
304
+ </div>
305
+ <p class="api__lede">
306
+ Every decorator maps directly onto a built-in browser primitive —
307
+ attributes, properties, events, MutationObserver. No magic, no virtual DOM,
308
+ no compiler required.
309
+ </p>
310
+ </div>
311
+
312
+ <div class="api__grid">
313
+ <div class="api-card">
314
+ <div class="api-card__name">@CustomElement<span class="target">class</span></div>
315
+ <p class="api-card__desc">Registers the class as a native custom element with a tag, optional template, and styles.</p>
316
+ </div>
317
+ <div class="api-card">
318
+ <div class="api-card__name">@Prop<span class="target">property</span></div>
319
+ <p class="api-card__desc">Reflects a primitive between attribute and property; passes objects and arrays through untouched.</p>
320
+ </div>
321
+ <div class="api-card">
322
+ <div class="api-card__name">@State<span class="target">property</span></div>
323
+ <p class="api-card__desc">Internal reactive state for <code>render()</code> components. Deeply proxied — mutate nested arrays and objects.</p>
324
+ </div>
325
+ <div class="api-card">
326
+ <div class="api-card__name">@Toggle<span class="target">property</span></div>
327
+ <p class="api-card__desc">A boolean attribute that follows the W3C spec — present, "true", or "false".</p>
328
+ </div>
329
+ <div class="api-card">
330
+ <div class="api-card__name">@Watch<span class="target">method</span></div>
331
+ <p class="api-card__desc">Runs a method whenever a watched property changes — from either side of the bridge.</p>
332
+ </div>
333
+ <div class="api-card">
334
+ <div class="api-card__name">@Listen<span class="target">method</span></div>
335
+ <p class="api-card__desc">Wires up DOM events on the host or on any selector inside the shadow tree.</p>
336
+ </div>
337
+ <div class="api-card">
338
+ <div class="api-card__name">@Dispatch<span class="target">property</span></div>
339
+ <p class="api-card__desc">Declares a typed CustomEvent emitter — call <code>.emit()</code> to fire it with a payload.</p>
340
+ </div>
341
+ </div>
342
+ </div>
343
+ </section>
344
+ </main>
345
+
346
+ <!-- ─── Footer ────────────────────────────────────────────── -->
347
+ <footer class="footer">
348
+ <div class="container footer__inner">
349
+ <span>MIT licensed · Maintained by Aivan Monceller &amp; contributors</span>
350
+ <span class="footer__links">
351
+ <a href="https://github.com/geocine/custom-elements-ts">GitHub</a>
352
+ <a href="https://www.npmjs.com/package/custom-elements-ts">npm</a>
353
+ <a href="https://github.com/geocine/custom-elements-ts/issues">Issues</a>
354
+ <a href="https://github.com/geocine/custom-elements-ts/blob/master/LICENSE">License</a>
355
+ </span>
356
+ </div>
357
+ </footer>
358
+
359
+ <!-- Toast notifications (the element subscribes to `cts:toast` document events) -->
360
+ <cts-toast></cts-toast>
361
+
362
+ <script src="//unpkg.com/prismjs@1.29.0/prism.js"></script>
363
+ <script src="//unpkg.com/prismjs@1.29.0/components/prism-typescript.min.js"></script>
364
+ <script src="//unpkg.com/@webcomponents/custom-elements/src/native-shim.js"></script>
365
+ <script src="//unpkg.com/@webcomponents/custom-elements/custom-elements.min.js"></script>
366
+ <script src="site.umd.js"></script>
367
+ </body>
368
+ </html>
@@ -0,0 +1,19 @@
1
+ // Site-only chrome / utility components.
2
+ export * from './code-example/code-example.element';
3
+ export * from './message/message.element';
4
+ export * from './toast/toast.element';
5
+ export * from './event-log/event-log.element';
6
+ export * from './source-viewer/source-viewer.element';
7
+
8
+ // Side-effect import: binds the [Source] toggle buttons to their viewers.
9
+ import './source-toggle/source-toggle';
10
+ // Side-effect import: prevents refresh from restoring a mid-page scroll.
11
+ import './scroll-restoration';
12
+
13
+ // Live demos hosted from sibling demo folders so visitors can interact
14
+ // with the real components, not screenshots.
15
+ export * from '../counter/counter.element';
16
+ export * from '../todo-dashboard/todo-stats.element';
17
+ export * from '../todo-dashboard/todo-filters.element';
18
+ export * from '../todo-dashboard/todo-item.element';
19
+ export * from '../todo-dashboard/todo-dashboard.element';
@@ -0,0 +1,53 @@
1
+ # custom-elements-ts
2
+
3
+ > A tiny (~3 kB min+gzip), zero-dependency, framework-free TypeScript library for authoring native Web Components with a small set of decorators (`@CustomElement`, `@Prop`, `@State`, `@Watch`, `@Listen`, `@Dispatch`, `@Toggle`) and an optional `html` / `render()` runtime.
4
+
5
+ Components built with custom-elements-ts are standards-compliant native Custom Elements: they work in plain HTML and inside any framework (React, Vue, Svelte, Angular, etc.). Two authoring styles compose freely:
6
+
7
+ - **Plain HTML, imperative updates.** Declare a `template` (or `templateUrl`) and update the DOM yourself in `connectedCallback`, `@Watch()`, or `@Listen()` handlers. No `render()`, no reactive runtime, zero runtime cost beyond the decorators. Use this for mostly-static UI like buttons, badges, panels, copy pills.
8
+ - **Reactive `render()` with the `html` helper.** Define `render()` returning an `html` tagged template. The runtime re-renders efficiently on `@Prop()` / `@State()` / `@Toggle()` changes, batching multiple synchronous mutations into one render. `render()` output is mounted into the shadow root by default; pass `shadow: false` to render into the host. State is deeply proxied — nested mutations (`this.user.name = '…'`, `this.items.push(…)`) schedule a render; only plain objects and arrays are proxied (functions, classes, DOM nodes, `Date`, `Map`, `Set`, `WeakMap`, `WeakSet` are left as-is).
9
+
10
+ Install: `npm install custom-elements-ts`. License: MIT. Repository: https://github.com/geocine/custom-elements-ts. Live demo site: https://geocine.github.io/custom-elements-ts/.
11
+
12
+ ## Documentation
13
+
14
+ - [README — full guide, decorators, examples](https://github.com/geocine/custom-elements-ts/blob/master/README.md): Quick start (plain HTML and reactive `render()`), template binding forms (`${expr}`, `${() => expr}`, `@event`, `.prop`, attribute, list rendering), every decorator (`@CustomElement`, `@Prop`, `@State`, `@Toggle`, `@Dispatch`, `@Watch`, `@Listen`), `@Toggle` boolean-attribute semantics table, project layout, and how to run / build the demos.
15
+ - [DeepWiki — AI-powered docs and Q&A](https://deepwiki.com/geocine/custom-elements-ts): Searchable conceptual documentation generated from the source; useful for asking natural-language questions about the runtime.
16
+ - [npm package page](https://www.npmjs.com/package/custom-elements-ts): Published artifacts (UMD + ESM with typings) and version history.
17
+ - [MIT license](https://github.com/geocine/custom-elements-ts/blob/master/LICENSE): Full license text.
18
+
19
+ ## Decorators (cheat sheet)
20
+
21
+ - `@CustomElement({ tag, template?, templateUrl?, style?, styleUrl?, shadow?: boolean })`: Registers the class as a native custom element. Defaults to shadow DOM.
22
+ - `@Prop()`: Public attribute/property. Reflects primitives (string, number, boolean) between attribute and property; objects, arrays, functions, classes are property-only (not reflected to attributes).
23
+ - `@State()`: Internal reactive state for `render()`-based components. Not reflected to attributes, not in `observedAttributes`. Plain objects and arrays are deeply proxied so nested mutations trigger a render.
24
+ - `@Toggle()`: Boolean attribute/property following the W3C boolean-attribute spec, with the convention that the literal string `"false"` evaluates to `false` and an unrecognized non-empty string also evaluates to `false`.
25
+ - `@Dispatch(eventName?)`: Declares a `DispatchEmitter` field. Call `.emit({ detail, bubbles?, composed?, cancelable? })` to fire a `CustomEvent`. Default event name is the property name with dots (`onChange` → `on.change`); pass an explicit name to override (e.g. `@Dispatch('counter.change')`). Use `bubbles: true, composed: true` to cross the shadow boundary.
26
+ - `@Watch(propertyName)`: Method runs whenever `propertyName` changes (from either attribute or property side). Receives `{ old, new }`.
27
+ - `@Listen(event, selector?)`: Wires up a DOM event on the host element, or on `selector` inside the shadow tree (any `querySelector`-compatible selector).
28
+
29
+ ## Source
30
+
31
+ - [src/](https://github.com/geocine/custom-elements-ts/tree/master/src): Library source — decorators plus the `html` / `render()` runtime.
32
+ - [src/index.ts](https://github.com/geocine/custom-elements-ts/blob/master/src/index.ts): Public entry point — every named export consumers can import.
33
+ - [tests/](https://github.com/geocine/custom-elements-ts/tree/master/tests): Vitest specs covering the runtime and every decorator. Authoritative behavioural reference when the README is ambiguous.
34
+ - [tests/templating-runtime.spec.ts](https://github.com/geocine/custom-elements-ts/blob/master/tests/templating-runtime.spec.ts): Most thorough spec for the `html` tagged template / `render()` runtime, covering binding forms, list rendering, attribute/property/event bindings, and disposal.
35
+
36
+ ## Demos
37
+
38
+ - [demos/counter/counter.element.ts](https://github.com/geocine/custom-elements-ts/blob/master/demos/counter/counter.element.ts): Minimal `@State()` + `render()` + `@Watch()` + `@Dispatch()` example — single-button card that emits `counter.change` on increment.
39
+ - [demos/todo-dashboard/todo-dashboard.element.ts](https://github.com/geocine/custom-elements-ts/blob/master/demos/todo-dashboard/todo-dashboard.element.ts): Parent dashboard owning the source of truth, composing child elements via bubbling `CustomEvent`s and deeply-proxied state.
40
+ - [demos/todo-dashboard/todo-item.element.ts](https://github.com/geocine/custom-elements-ts/blob/master/demos/todo-dashboard/todo-item.element.ts): Child row component — `@Prop()` inputs, `@Listen()` handlers, `@Dispatch()` outputs.
41
+ - [demos/todo-dashboard/todo-stats.element.ts](https://github.com/geocine/custom-elements-ts/blob/master/demos/todo-dashboard/todo-stats.element.ts): Sibling stats card driven entirely by `@Prop()` data passed down from the parent.
42
+ - [demos/site/](https://github.com/geocine/custom-elements-ts/tree/master/demos/site): Source for the showcase landing page itself — `<cts-message>` (plain-HTML, imperative), `<cts-toast>`, `<cts-event-log>`, `<cts-code-example>`. Demonstrates that the same library is used to build its own marketing site.
43
+ - [demos/site/message/message.element.ts](https://github.com/geocine/custom-elements-ts/blob/master/demos/site/message/message.element.ts): Canonical "no `render()` required" example — static `template` + imperative DOM updates in `connectedCallback`, `@Listen('click')`, and a bubbling+composed `@Dispatch('cts:toast')`.
44
+
45
+ ## Live site (this domain)
46
+
47
+ - [Showcase landing page](https://geocine.github.io/custom-elements-ts/): Hero, install pill, live counter demo, live sprint-board demo, and an event-log panel that subscribes to bubbling `CustomEvent`s in real time. All demos on the page are the *real* compiled components, not screenshots.
48
+
49
+ ## Optional
50
+
51
+ - [GitHub issues](https://github.com/geocine/custom-elements-ts/issues): Bug reports and feature requests.
52
+ - [Coveralls — test coverage](https://coveralls.io/github/geocine/custom-elements-ts?branch=master): Current line coverage for the library.
53
+ - [GitHub Actions — CI](https://github.com/geocine/custom-elements-ts/actions/workflows/ci.yml): Build, test, and GitHub Pages deploy pipeline.
@@ -0,0 +1,75 @@
1
+ :host {
2
+ display: block;
3
+ margin: 18px 22px 6px;
4
+ --bg: rgba(255, 255, 255, 0.025);
5
+ --bg-hover: rgba(255, 255, 255, 0.05);
6
+ --line: rgba(255, 255, 255, 0.10);
7
+ --line-hover: rgba(49, 120, 198, 0.45);
8
+ --fg: #e6ecf3;
9
+ --fg-prompt: #9cc0eb;
10
+ --fg-icon: #5b6370;
11
+ --fg-icon-hover: #9cc0eb;
12
+ }
13
+
14
+ .row {
15
+ display: grid;
16
+ grid-template-columns: auto minmax(0, 1fr) auto;
17
+ align-items: center;
18
+ gap: 10px;
19
+ padding: 13px 14px;
20
+ font-family: "Geist Mono", "JetBrains Mono", ui-monospace, SFMono-Regular,
21
+ Menlo, Consolas, "Courier New", monospace;
22
+ font-size: 13.5px;
23
+ font-weight: 500;
24
+ color: var(--fg);
25
+ background: var(--bg);
26
+ border: 1px solid var(--line);
27
+ border-radius: 3px;
28
+ cursor: pointer;
29
+ user-select: none;
30
+ outline: none;
31
+ transition: background 220ms cubic-bezier(0.22, 1, 0.36, 1),
32
+ border-color 220ms cubic-bezier(0.22, 1, 0.36, 1),
33
+ transform 140ms cubic-bezier(0.22, 1, 0.36, 1);
34
+ }
35
+
36
+ .row:hover {
37
+ background: var(--bg-hover);
38
+ border-color: var(--line-hover);
39
+ }
40
+
41
+ .row:active { transform: translateY(1px); }
42
+
43
+ .row:focus-visible {
44
+ border-color: var(--line-hover);
45
+ box-shadow: 0 0 0 2px rgba(49, 120, 198, 0.45);
46
+ }
47
+
48
+ .prompt {
49
+ color: var(--fg-prompt);
50
+ font-weight: 600;
51
+ opacity: 0.95;
52
+ }
53
+
54
+ .cmd {
55
+ font-family: inherit;
56
+ overflow: hidden;
57
+ text-overflow: ellipsis;
58
+ white-space: nowrap;
59
+ min-width: 0;
60
+ }
61
+
62
+ .icon {
63
+ display: grid;
64
+ place-items: center;
65
+ color: var(--fg-icon);
66
+ transition: color 220ms cubic-bezier(0.22, 1, 0.36, 1);
67
+ }
68
+
69
+ .icon svg {
70
+ width: 14px;
71
+ height: 14px;
72
+ display: block;
73
+ }
74
+
75
+ .row:hover .icon { color: var(--fg-icon-hover); }
@@ -0,0 +1,76 @@
1
+ import { CustomElement, Prop, Listen, Dispatch, DispatchEmitter } from 'custom-elements-ts';
2
+
3
+ @CustomElement({
4
+ tag: 'cts-message',
5
+ template: `
6
+ <div class="row" role="button" tabindex="0" aria-label="Copy install command to clipboard">
7
+ <span class="prompt" aria-hidden="true">$</span>
8
+ <code class="cmd"></code>
9
+ <span class="icon" aria-hidden="true">
10
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
11
+ <rect x="9" y="9" width="13" height="13" rx="1"/>
12
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
13
+ </svg>
14
+ </span>
15
+ </div>
16
+ `,
17
+ styleUrl: './message.element.scss',
18
+ })
19
+ export class MessageElement extends HTMLElement {
20
+ @Prop() message!: string;
21
+
22
+ @Dispatch('cts:toast') toast!: DispatchEmitter;
23
+
24
+ @Listen('click')
25
+ async handleClick() {
26
+ const text = this.message ?? '';
27
+ let ok = true;
28
+ try {
29
+ if (navigator.clipboard?.writeText) {
30
+ await navigator.clipboard.writeText(text);
31
+ } else {
32
+ ok = this.fallbackCopy(text);
33
+ }
34
+ } catch {
35
+ ok = false;
36
+ }
37
+ this.toast.emit({
38
+ bubbles: true,
39
+ composed: true,
40
+ detail: ok
41
+ ? { title: 'Copied to clipboard', message: text, kind: 'success' }
42
+ : { title: "Couldn't copy", message: 'Copy the command manually.', kind: 'error' },
43
+ });
44
+ }
45
+
46
+ @Listen('keydown')
47
+ handleKey(e: KeyboardEvent) {
48
+ if (e.key === 'Enter' || e.key === ' ') {
49
+ e.preventDefault();
50
+ this.handleClick();
51
+ }
52
+ }
53
+
54
+ connectedCallback() {
55
+ const cmd = this.shadowRoot?.querySelector('.cmd');
56
+ if (cmd) cmd.textContent = this.message;
57
+ }
58
+
59
+ // Older browsers / non-secure contexts: fall back to the legacy copy path.
60
+ private fallbackCopy(text: string): boolean {
61
+ const ta = document.createElement('textarea');
62
+ ta.value = text;
63
+ ta.style.position = 'fixed';
64
+ ta.style.opacity = '0';
65
+ document.body.appendChild(ta);
66
+ ta.select();
67
+ let ok = false;
68
+ try {
69
+ ok = document.execCommand('copy');
70
+ } catch {
71
+ ok = false;
72
+ }
73
+ document.body.removeChild(ta);
74
+ return ok;
75
+ }
76
+ }
Binary file
@@ -0,0 +1,28 @@
1
+ function scrollToHashTarget(): boolean {
2
+ const id = decodeURIComponent(window.location.hash.slice(1));
3
+ const target = id ? document.getElementById(id) : null;
4
+ if (!target) return false;
5
+ target.scrollIntoView();
6
+ return true;
7
+ }
8
+
9
+ function syncInitialScroll() {
10
+ if (typeof window === 'undefined') return;
11
+
12
+ if (window.location.hash) {
13
+ if ('scrollRestoration' in window.history) {
14
+ window.history.scrollRestoration = 'auto';
15
+ }
16
+ requestAnimationFrame(scrollToHashTarget);
17
+ return;
18
+ }
19
+
20
+ if ('scrollRestoration' in window.history) {
21
+ window.history.scrollRestoration = 'manual';
22
+ }
23
+
24
+ window.scrollTo({ top: 0, left: 0, behavior: 'auto' });
25
+ }
26
+
27
+ syncInitialScroll();
28
+ window.addEventListener('pageshow', syncInitialScroll);