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,1135 @@
1
+ @import url("./tokens.css");
2
+
3
+ /* ─────────────────────────────────────────────────────────────
4
+ Reset & base
5
+ ───────────────────────────────────────────────────────────── */
6
+
7
+ *, *::before, *::after { box-sizing: border-box; }
8
+
9
+ html, body {
10
+ margin: 0;
11
+ padding: 0;
12
+ background: var(--bg);
13
+ color: var(--fg);
14
+ font-family: var(--font-sans);
15
+ font-size: 16px;
16
+ line-height: 1.55;
17
+ -webkit-font-smoothing: antialiased;
18
+ -moz-osx-font-smoothing: grayscale;
19
+ text-rendering: optimizeLegibility;
20
+ font-feature-settings: "ss01", "cv11";
21
+ }
22
+
23
+ html { scroll-behavior: smooth; }
24
+
25
+ body {
26
+ /* Cool spotlight behind the hero. Single source of light. */
27
+ background:
28
+ radial-gradient(1200px 600px at 50% -120px,
29
+ rgba(49, 120, 198, 0.14) 0%,
30
+ rgba(49, 120, 198, 0) 60%),
31
+ radial-gradient(900px 500px at 85% 10%,
32
+ rgba(255, 255, 255, 0.04) 0%,
33
+ transparent 60%),
34
+ var(--bg);
35
+ min-height: 100dvh;
36
+ overflow-x: hidden;
37
+ }
38
+
39
+ /* Subtle film-grain overlay so flat surfaces don't feel sterile. */
40
+ body::before {
41
+ content: "";
42
+ position: fixed;
43
+ inset: 0;
44
+ pointer-events: none;
45
+ z-index: 1;
46
+ opacity: 0.035;
47
+ mix-blend-mode: overlay;
48
+ background-image:
49
+ url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.6'/></svg>");
50
+ }
51
+
52
+ a {
53
+ color: inherit;
54
+ text-decoration: none;
55
+ transition: color var(--dur) var(--ease);
56
+ }
57
+ a:hover { color: var(--accent); }
58
+
59
+ ::selection {
60
+ background: var(--accent-soft);
61
+ color: var(--fg);
62
+ }
63
+
64
+ /* Visible focus for keyboard users only */
65
+ :focus-visible {
66
+ outline: 2px solid var(--accent);
67
+ outline-offset: 3px;
68
+ border-radius: var(--radius-sm);
69
+ }
70
+
71
+ /* Skip-to-content for keyboard navigation */
72
+ .skip-link {
73
+ position: absolute;
74
+ left: -9999px;
75
+ top: 0;
76
+ background: var(--accent);
77
+ color: #fff;
78
+ padding: 10px 14px;
79
+ border-radius: var(--radius-sm);
80
+ font-weight: 600;
81
+ z-index: 100;
82
+ }
83
+ .skip-link:focus { left: 16px; top: 16px; }
84
+
85
+ /* ─────────────────────────────────────────────────────────────
86
+ Layout primitives
87
+ ───────────────────────────────────────────────────────────── */
88
+
89
+ .container {
90
+ width: 100%;
91
+ max-width: var(--container);
92
+ margin: 0 auto;
93
+ padding-inline: var(--gutter);
94
+ position: relative;
95
+ z-index: 2;
96
+ }
97
+
98
+ .eyebrow {
99
+ display: inline-flex;
100
+ align-items: center;
101
+ gap: 8px;
102
+ font-family: var(--font-mono);
103
+ font-size: 12px;
104
+ letter-spacing: 0.04em;
105
+ text-transform: uppercase;
106
+ color: var(--fg-muted);
107
+ }
108
+ .eyebrow .dot {
109
+ width: 6px;
110
+ height: 6px;
111
+ background: var(--accent);
112
+ box-shadow: 0 0 12px var(--accent-line);
113
+ }
114
+
115
+ /* ─────────────────────────────────────────────────────────────
116
+ Top navigation
117
+ ───────────────────────────────────────────────────────────── */
118
+
119
+ .nav {
120
+ position: sticky;
121
+ top: 0;
122
+ z-index: 20;
123
+ backdrop-filter: saturate(140%) blur(14px);
124
+ -webkit-backdrop-filter: saturate(140%) blur(14px);
125
+ background: linear-gradient(to bottom,
126
+ rgba(11, 11, 12, 0.78) 0%,
127
+ rgba(11, 11, 12, 0.55) 100%);
128
+ border-bottom: 1px solid var(--line);
129
+ }
130
+
131
+ .nav__inner {
132
+ display: flex;
133
+ align-items: center;
134
+ justify-content: space-between;
135
+ height: 64px;
136
+ gap: 24px;
137
+ }
138
+
139
+ .brand {
140
+ display: inline-flex;
141
+ align-items: center;
142
+ gap: 10px;
143
+ font-weight: 600;
144
+ letter-spacing: -0.01em;
145
+ color: var(--fg);
146
+ }
147
+ .brand:hover { color: var(--fg); }
148
+
149
+ .brand__mark {
150
+ width: 28px;
151
+ height: 28px;
152
+ display: grid;
153
+ place-items: center;
154
+ border-radius: var(--radius-sm);
155
+ background: linear-gradient(160deg, #1c2230 0%, #11141a 100%);
156
+ border: 1px solid var(--line-strong);
157
+ color: var(--accent-fg);
158
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
159
+ }
160
+ .brand__mark svg {
161
+ display: block;
162
+ width: 16px;
163
+ height: 16px;
164
+ }
165
+
166
+ .brand__name {
167
+ font-family: var(--font-mono);
168
+ font-size: 14px;
169
+ color: var(--fg);
170
+ white-space: nowrap;
171
+ }
172
+ .brand__name .dim { color: var(--fg-subtle); }
173
+
174
+ .nav__links {
175
+ display: none;
176
+ align-items: center;
177
+ gap: 6px;
178
+ }
179
+ @media (min-width: 760px) {
180
+ .nav__links { display: inline-flex; }
181
+ }
182
+
183
+ .nav__link {
184
+ padding: 8px 12px;
185
+ border-radius: var(--radius-sm);
186
+ font-size: 14px;
187
+ color: var(--fg-muted);
188
+ }
189
+ .nav__link:hover {
190
+ color: var(--fg);
191
+ background: var(--bg-elev-1);
192
+ }
193
+
194
+ .nav__cta {
195
+ display: inline-flex;
196
+ align-items: center;
197
+ gap: 8px;
198
+ padding: 8px 12px;
199
+ border-radius: var(--radius-sm);
200
+ border: 1px solid var(--line-strong);
201
+ background: var(--bg-elev-1);
202
+ font-family: var(--font-mono);
203
+ font-size: 12px;
204
+ color: var(--fg);
205
+ transition: background var(--dur) var(--ease),
206
+ border-color var(--dur) var(--ease),
207
+ transform var(--dur) var(--ease);
208
+ }
209
+ .nav__cta:hover {
210
+ background: var(--bg-elev-2);
211
+ border-color: var(--line-accent);
212
+ color: var(--fg);
213
+ }
214
+ .nav__cta:active { transform: translateY(1px); }
215
+ .nav__cta svg { width: 14px; height: 14px; }
216
+ .nav__cta .stars {
217
+ color: var(--accent);
218
+ font-variant-numeric: tabular-nums;
219
+ }
220
+
221
+ /* ─────────────────────────────────────────────────────────────
222
+ Hero
223
+ ───────────────────────────────────────────────────────────── */
224
+
225
+ .hero {
226
+ padding: clamp(56px, 9vw, 110px) 0 clamp(40px, 6vw, 72px);
227
+ position: relative;
228
+ }
229
+
230
+ .hero__pill {
231
+ display: inline-flex;
232
+ align-items: center;
233
+ gap: 10px;
234
+ padding: 6px 12px 6px 8px;
235
+ border-radius: var(--radius-sm);
236
+ border: 1px solid var(--line-strong);
237
+ background: var(--bg-elev-1);
238
+ font-family: var(--font-mono);
239
+ font-size: 12px;
240
+ color: var(--fg-muted);
241
+ font-variant-numeric: tabular-nums;
242
+ white-space: nowrap;
243
+ }
244
+ .hero__pill .badge {
245
+ display: inline-flex;
246
+ align-items: center;
247
+ padding: 2px 8px;
248
+ border-radius: var(--radius-xs);
249
+ background: var(--accent-soft);
250
+ color: var(--accent-fg);
251
+ font-weight: 600;
252
+ letter-spacing: 0.02em;
253
+ }
254
+ .hero__pill .sep { color: var(--fg-subtle); }
255
+
256
+ .hero__title {
257
+ margin: 22px 0 18px;
258
+ font-size: clamp(40px, 6.4vw, 76px);
259
+ line-height: 1.02;
260
+ letter-spacing: -0.035em;
261
+ font-weight: 600;
262
+ color: var(--fg);
263
+ max-width: 14ch;
264
+ text-wrap: balance;
265
+ }
266
+ .hero__title .em {
267
+ font-style: normal;
268
+ color: var(--accent-fg);
269
+ }
270
+
271
+ .hero__lede {
272
+ margin: 0 0 32px;
273
+ max-width: 58ch;
274
+ font-size: clamp(16px, 1.4vw, 19px);
275
+ line-height: 1.55;
276
+ color: var(--fg-muted);
277
+ text-wrap: pretty;
278
+ }
279
+ .hero__lede strong {
280
+ color: var(--fg);
281
+ font-weight: 500;
282
+ }
283
+
284
+ .hero__actions {
285
+ display: flex;
286
+ flex-wrap: wrap;
287
+ gap: 12px;
288
+ margin-bottom: 28px;
289
+ }
290
+
291
+ .btn {
292
+ display: inline-flex;
293
+ align-items: center;
294
+ gap: 8px;
295
+ height: 42px;
296
+ padding: 0 18px;
297
+ border-radius: var(--radius-md);
298
+ font-family: var(--font-sans);
299
+ font-size: 14px;
300
+ font-weight: 500;
301
+ cursor: pointer;
302
+ border: 1px solid transparent;
303
+ transition: background var(--dur) var(--ease),
304
+ color var(--dur) var(--ease),
305
+ border-color var(--dur) var(--ease),
306
+ transform var(--dur-fast) var(--ease);
307
+ white-space: nowrap;
308
+ }
309
+ .btn:active { transform: translateY(1px); }
310
+
311
+ .btn--primary {
312
+ background: var(--accent);
313
+ color: #ffffff;
314
+ border-color: var(--accent);
315
+ box-shadow:
316
+ 0 1px 0 rgba(255, 255, 255, 0.18) inset,
317
+ 0 10px 24px -12px rgba(49, 120, 198, 0.65);
318
+ }
319
+ .btn--primary:hover {
320
+ background: var(--accent-bright);
321
+ color: #ffffff;
322
+ border-color: var(--accent-bright);
323
+ }
324
+
325
+ .btn--ghost {
326
+ background: transparent;
327
+ color: var(--fg);
328
+ border-color: var(--line-strong);
329
+ }
330
+ .btn--ghost:hover {
331
+ background: var(--bg-elev-1);
332
+ border-color: var(--line-accent);
333
+ color: var(--fg);
334
+ }
335
+
336
+ .btn .arrow {
337
+ width: 14px; height: 14px;
338
+ transition: transform var(--dur) var(--ease);
339
+ }
340
+ .btn:hover .arrow { transform: translateX(3px); }
341
+
342
+ .hero__meta {
343
+ display: flex;
344
+ flex-wrap: wrap;
345
+ gap: 22px;
346
+ font-family: var(--font-mono);
347
+ font-size: 12px;
348
+ color: var(--fg-subtle);
349
+ }
350
+ .hero__meta .ok { color: var(--ok); }
351
+
352
+ /* ─────────────────────────────────────────────────────────────
353
+ Showcase: code preview + key points (asymmetric)
354
+ ───────────────────────────────────────────────────────────── */
355
+
356
+ .showcase {
357
+ padding-bottom: clamp(60px, 8vw, 96px);
358
+ }
359
+
360
+ .showcase__grid {
361
+ display: grid;
362
+ gap: 24px;
363
+ grid-template-columns: 1fr;
364
+ }
365
+ @media (min-width: 980px) {
366
+ .showcase__grid {
367
+ grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr);
368
+ gap: 32px;
369
+ align-items: stretch;
370
+ }
371
+ }
372
+
373
+ /* Window-chrome wrapper around the code element */
374
+ .window {
375
+ position: relative;
376
+ border-radius: var(--radius-lg);
377
+ border: 1px solid var(--line-strong);
378
+ background: linear-gradient(180deg, #131720 0%, #0c0f15 100%);
379
+ overflow: hidden;
380
+ box-shadow:
381
+ 0 1px 0 rgba(255, 255, 255, 0.04) inset,
382
+ 0 30px 60px -30px rgba(0, 0, 0, 0.55);
383
+ }
384
+ .window__bar {
385
+ display: flex;
386
+ align-items: center;
387
+ gap: 10px;
388
+ padding: 10px 14px;
389
+ border-bottom: 1px solid var(--line);
390
+ background: linear-gradient(180deg, #181d27 0%, #11141a 100%);
391
+ }
392
+ .window__dots {
393
+ display: inline-flex;
394
+ gap: 6px;
395
+ }
396
+ .window__dots span {
397
+ width: 9px;
398
+ height: 9px;
399
+ border-radius: var(--radius-xs);
400
+ background: #2c313b;
401
+ border: 1px solid rgba(255, 255, 255, 0.04);
402
+ }
403
+ .window__dots span:nth-child(1) { background: #3a4150; }
404
+ .window__dots span:nth-child(2) { background: #2e3540; }
405
+ .window__dots span:nth-child(3) { background: #232932; }
406
+
407
+ .window__title {
408
+ margin-left: 8px;
409
+ font-family: var(--font-mono);
410
+ font-size: 12px;
411
+ color: var(--fg-subtle);
412
+ }
413
+ .window__title .accent { color: var(--accent-fg); }
414
+
415
+ .window__body {
416
+ background: #0b0e14;
417
+ }
418
+
419
+ /* Side panel: install + checks + CTA */
420
+ .side-card {
421
+ display: flex;
422
+ flex-direction: column;
423
+ border-radius: var(--radius-lg);
424
+ border: 1px solid var(--line-strong);
425
+ background:
426
+ radial-gradient(180px 120px at 100% 0%,
427
+ rgba(49, 120, 198, 0.14) 0%,
428
+ transparent 70%),
429
+ linear-gradient(180deg, #131720 0%, #0c0f15 100%);
430
+ overflow: hidden;
431
+ }
432
+
433
+ .side-card__head {
434
+ padding: 22px 22px 6px;
435
+ }
436
+ .side-card__kicker {
437
+ font-family: var(--font-mono);
438
+ font-size: 11px;
439
+ letter-spacing: 0.06em;
440
+ text-transform: uppercase;
441
+ color: var(--accent-fg);
442
+ }
443
+ .side-card__title {
444
+ margin: 6px 0 0;
445
+ font-size: 22px;
446
+ font-weight: 600;
447
+ letter-spacing: -0.01em;
448
+ color: var(--fg);
449
+ }
450
+ .side-card__sub {
451
+ margin: 10px 0 16px;
452
+ color: var(--fg-muted);
453
+ font-size: 13.5px;
454
+ line-height: 1.55;
455
+ text-wrap: pretty;
456
+ }
457
+ .side-card__sub em {
458
+ font-style: normal;
459
+ color: var(--fg);
460
+ }
461
+ .side-card__sub code {
462
+ padding: 1px 6px;
463
+ border: 1px solid var(--line);
464
+ border-radius: var(--radius-xs);
465
+ background: rgba(255, 255, 255, 0.025);
466
+ font-family: var(--font-mono);
467
+ font-size: 12px;
468
+ color: var(--accent-fg);
469
+ }
470
+
471
+ /* Checklist */
472
+ .checks {
473
+ list-style: none;
474
+ margin: 0;
475
+ padding: 18px 22px 6px;
476
+ display: grid;
477
+ gap: 12px;
478
+ }
479
+ .checks li {
480
+ display: flex;
481
+ align-items: flex-start;
482
+ gap: 12px;
483
+ font-size: 14px;
484
+ color: var(--fg-muted);
485
+ }
486
+ .checks li strong {
487
+ color: var(--fg);
488
+ font-weight: 500;
489
+ }
490
+ .checks .check {
491
+ flex: 0 0 auto;
492
+ width: 18px;
493
+ height: 18px;
494
+ margin-top: 2px;
495
+ border-radius: var(--radius-xs);
496
+ background: var(--accent-soft);
497
+ color: var(--accent-fg);
498
+ display: grid;
499
+ place-items: center;
500
+ }
501
+ .checks .check svg { width: 11px; height: 11px; }
502
+
503
+ .side-card__foot {
504
+ margin-top: auto;
505
+ padding: 18px 22px 22px;
506
+ border-top: 1px solid var(--line);
507
+ background: rgba(255, 255, 255, 0.015);
508
+ }
509
+ .side-card__foot a {
510
+ display: inline-flex;
511
+ align-items: center;
512
+ gap: 8px;
513
+ font-family: var(--font-mono);
514
+ font-size: 13px;
515
+ color: var(--fg);
516
+ }
517
+ .side-card__foot a .arrow { transition: transform var(--dur) var(--ease); }
518
+ .side-card__foot a:hover .arrow { transform: translateX(3px); }
519
+
520
+ /* ─────────────────────────────────────────────────────────────
521
+ Live demos
522
+ Two playgrounds (counter, todo dashboard) hosted alongside
523
+ real <cts-event-log> panels so visitors can watch the events
524
+ the components emit in real time.
525
+ ───────────────────────────────────────────────────────────── */
526
+
527
+ .live {
528
+ padding: clamp(60px, 8vw, 96px) 0 clamp(40px, 6vw, 72px);
529
+ border-top: 1px solid var(--line);
530
+ position: relative;
531
+ }
532
+
533
+ .live__head {
534
+ display: flex;
535
+ align-items: end;
536
+ justify-content: space-between;
537
+ gap: 28px;
538
+ flex-wrap: wrap;
539
+ margin-bottom: 36px;
540
+ }
541
+ .live__title {
542
+ margin: 12px 0 0;
543
+ font-size: clamp(28px, 3.4vw, 38px);
544
+ font-weight: 600;
545
+ letter-spacing: -0.02em;
546
+ max-width: 22ch;
547
+ text-wrap: balance;
548
+ }
549
+ .live__lede {
550
+ max-width: 50ch;
551
+ color: var(--fg-muted);
552
+ font-size: 15px;
553
+ text-wrap: pretty;
554
+ margin: 0;
555
+ }
556
+ .live__lede code {
557
+ padding: 1px 6px;
558
+ border: 1px solid var(--line);
559
+ border-radius: var(--radius-xs);
560
+ background: rgba(255, 255, 255, 0.025);
561
+ font-family: var(--font-mono);
562
+ font-size: 12.5px;
563
+ color: var(--accent-fg);
564
+ }
565
+
566
+ /* Keep inline code chips like `npm install` or `@State()` atomic so
567
+ prose wraps around them instead of breaking them mid-token. */
568
+ :where(.hero__lede, .live__lede, .side-card__sub, .playground__title,
569
+ .playground__sub, .stage__caption, .api__lede) code {
570
+ white-space: nowrap;
571
+ }
572
+
573
+ .playground {
574
+ position: relative;
575
+ margin-top: 22px;
576
+ border-radius: var(--radius-lg);
577
+ border: 1px solid var(--line-strong);
578
+ background:
579
+ radial-gradient(160px 110px at 100% 0%,
580
+ rgba(49, 120, 198, 0.10) 0%,
581
+ transparent 70%),
582
+ linear-gradient(180deg, #131720 0%, #0c0f15 100%);
583
+ overflow: hidden;
584
+ box-shadow:
585
+ 0 1px 0 rgba(255, 255, 255, 0.04) inset,
586
+ 0 30px 60px -30px rgba(0, 0, 0, 0.55);
587
+ }
588
+
589
+ .playground__head {
590
+ display: flex;
591
+ align-items: flex-start;
592
+ justify-content: space-between;
593
+ gap: 18px;
594
+ padding: 22px 24px 18px;
595
+ border-bottom: 1px solid var(--line);
596
+ }
597
+ .playground__head > div {
598
+ min-width: 0;
599
+ }
600
+ .playground__kicker {
601
+ display: inline-block;
602
+ font-family: var(--font-mono);
603
+ font-size: 11px;
604
+ letter-spacing: 0.14em;
605
+ text-transform: uppercase;
606
+ color: var(--accent-fg);
607
+ }
608
+ .playground__title {
609
+ margin: 8px 0 0;
610
+ font-size: 20px;
611
+ font-weight: 600;
612
+ letter-spacing: -0.015em;
613
+ color: var(--fg);
614
+ text-wrap: balance;
615
+ }
616
+ .playground__title code {
617
+ padding: 1px 6px;
618
+ border: 1px solid var(--line);
619
+ border-radius: var(--radius-xs);
620
+ background: rgba(255, 255, 255, 0.025);
621
+ font-family: var(--font-mono);
622
+ font-size: 13px;
623
+ font-weight: 500;
624
+ color: var(--accent-fg);
625
+ }
626
+ .playground__sub {
627
+ margin: 10px 0 0;
628
+ max-width: 60ch;
629
+ color: var(--fg-muted);
630
+ font-size: 14px;
631
+ line-height: 1.6;
632
+ text-wrap: pretty;
633
+ }
634
+ .playground__sub code {
635
+ padding: 1px 5px;
636
+ border: 1px solid var(--line);
637
+ border-radius: var(--radius-xs);
638
+ background: rgba(255, 255, 255, 0.025);
639
+ font-family: var(--font-mono);
640
+ font-size: 12px;
641
+ color: var(--accent-fg);
642
+ }
643
+ /* Playground header actions. */
644
+ .playground__actions {
645
+ flex: 0 0 auto;
646
+ display: inline-flex;
647
+ align-items: center;
648
+ gap: 8px;
649
+ flex-wrap: wrap;
650
+ justify-content: flex-end;
651
+ }
652
+
653
+ .playground__btn {
654
+ flex: 0 0 auto;
655
+ display: inline-flex;
656
+ align-items: center;
657
+ gap: 8px;
658
+ padding: 8px 12px;
659
+ border: 1px solid var(--line-strong);
660
+ border-radius: var(--radius-xs);
661
+ background: rgba(255, 255, 255, 0.02);
662
+ font-family: var(--font-mono);
663
+ font-size: 12px;
664
+ color: var(--fg-muted);
665
+ white-space: nowrap;
666
+ cursor: pointer;
667
+ text-decoration: none;
668
+ transition: border-color var(--dur) var(--ease),
669
+ color var(--dur) var(--ease),
670
+ background var(--dur) var(--ease);
671
+ }
672
+ .playground__btn:hover {
673
+ color: var(--fg);
674
+ border-color: var(--line-accent);
675
+ background: var(--accent-soft);
676
+ }
677
+ .playground__btn:active {
678
+ transform: translateY(1px);
679
+ }
680
+ .playground__btn[aria-expanded="true"],
681
+ .playground__btn.is-active {
682
+ color: var(--accent-fg);
683
+ border-color: var(--line-accent);
684
+ background: var(--accent-soft);
685
+ }
686
+
687
+ .playground__btn-caret {
688
+ margin-left: 2px;
689
+ opacity: 0.6;
690
+ transition: transform var(--dur) var(--ease), opacity var(--dur) var(--ease);
691
+ }
692
+ .playground__btn[aria-expanded="true"] .playground__btn-caret {
693
+ transform: rotate(180deg);
694
+ opacity: 0.9;
695
+ }
696
+
697
+ .playground__btn--ghost {
698
+ background: transparent;
699
+ }
700
+ .playground__btn--ghost:hover {
701
+ background: var(--bg-elev-2);
702
+ }
703
+
704
+ /* Backwards compatibility for the original source link class. */
705
+ .playground__source {
706
+ flex: 0 0 auto;
707
+ display: inline-flex;
708
+ align-items: center;
709
+ gap: 8px;
710
+ padding: 8px 12px;
711
+ border: 1px solid var(--line-strong);
712
+ border-radius: var(--radius-xs);
713
+ background: rgba(255, 255, 255, 0.02);
714
+ font-family: var(--font-mono);
715
+ font-size: 12px;
716
+ color: var(--fg-muted);
717
+ white-space: nowrap;
718
+ transition: border-color var(--dur) var(--ease),
719
+ color var(--dur) var(--ease),
720
+ background var(--dur) var(--ease);
721
+ }
722
+ .playground__source:hover {
723
+ color: var(--fg);
724
+ border-color: var(--line-accent);
725
+ background: var(--accent-soft);
726
+ }
727
+
728
+ .playground__body {
729
+ display: grid;
730
+ gap: 20px;
731
+ padding: 26px 24px 28px;
732
+ }
733
+
734
+ /* Counter playground: stage on the left, log on the right at >=900px. */
735
+ .playground__body--split {
736
+ grid-template-columns: 1fr;
737
+ }
738
+ @media (min-width: 900px) {
739
+ .playground__body--split {
740
+ grid-template-columns: minmax(0, 1fr) minmax(280px, 360px);
741
+ align-items: stretch;
742
+ }
743
+ }
744
+
745
+ /* Todo needs a wider breakpoint than the counter card. */
746
+ .playground--todo .playground__body--split {
747
+ grid-template-columns: 1fr;
748
+ }
749
+ @media (min-width: 1024px) {
750
+ .playground--todo .playground__body--split {
751
+ grid-template-columns: minmax(0, 1fr) minmax(280px, 360px);
752
+ align-items: stretch;
753
+ }
754
+ }
755
+
756
+ /* Legacy stack variant for external embeds. */
757
+ .playground__body--stack {
758
+ grid-template-columns: 1fr;
759
+ }
760
+
761
+ .stage {
762
+ display: grid;
763
+ place-items: center;
764
+ gap: 18px;
765
+ padding: 28px 18px;
766
+ border: 1px solid var(--line);
767
+ border-radius: var(--radius-md);
768
+ background:
769
+ radial-gradient(60% 60% at 50% 0%,
770
+ rgba(49, 120, 198, 0.05) 0%,
771
+ transparent 60%),
772
+ rgba(0, 0, 0, 0.18);
773
+ background-image:
774
+ linear-gradient(to right, rgba(255, 255, 255, 0.022) 1px, transparent 1px),
775
+ linear-gradient(to bottom, rgba(255, 255, 255, 0.022) 1px, transparent 1px),
776
+ radial-gradient(60% 60% at 50% 0%,
777
+ rgba(49, 120, 198, 0.05) 0%,
778
+ transparent 60%),
779
+ linear-gradient(180deg, #0d1118 0%, #0a0d13 100%);
780
+ background-size: 28px 28px, 28px 28px, auto, auto;
781
+ min-height: 240px;
782
+ }
783
+
784
+ .stage--counter {
785
+ grid-template-rows: auto auto;
786
+ }
787
+
788
+ .stage--todo {
789
+ padding: 32px 18px;
790
+ }
791
+ /* Prevent host min-content overflow on narrow phones. */
792
+ .stage--todo cts-todo-dashboard {
793
+ width: min(540px, 100%);
794
+ min-width: 0;
795
+ }
796
+
797
+ .stage__caption {
798
+ margin: 0;
799
+ max-width: 38ch;
800
+ color: var(--fg-subtle);
801
+ font-size: 12.5px;
802
+ line-height: 1.55;
803
+ text-align: center;
804
+ text-wrap: balance;
805
+ }
806
+ .stage__caption code {
807
+ padding: 1px 5px;
808
+ border: 1px solid var(--line);
809
+ border-radius: var(--radius-xs);
810
+ background: rgba(255, 255, 255, 0.02);
811
+ font-family: var(--font-mono);
812
+ font-size: 11.5px;
813
+ color: var(--accent-fg);
814
+ }
815
+
816
+ .playground__log {
817
+ display: flex; /* lets the host stretch in grid contexts */
818
+ }
819
+ .playground__log--wide {
820
+ /* Sits below the dashboard; let it span the full row. */
821
+ width: 100%;
822
+ }
823
+
824
+ /* Do not set display here; the host grid drives the open/close animation. */
825
+ cts-source-viewer:not(:defined) {
826
+ /* Keep the collapsed state during hydration. */
827
+ display: block;
828
+ height: 0;
829
+ overflow: hidden;
830
+ }
831
+
832
+ /* Loading skeletons for live demos. */
833
+ cts-counter,
834
+ cts-todo-dashboard,
835
+ cts-event-log {
836
+ display: block;
837
+ }
838
+
839
+ cts-counter:not(:defined) {
840
+ width: 320px;
841
+ height: 230px;
842
+ border: 1px solid var(--line-strong);
843
+ border-radius: var(--radius-lg);
844
+ background: linear-gradient(180deg, #11141a 0%, #0f131b 100%);
845
+ }
846
+
847
+ cts-todo-dashboard:not(:defined) {
848
+ /* Match the compact hydrated width. */
849
+ width: min(540px, calc(100vw - 80px));
850
+ height: 520px;
851
+ border: 1px solid var(--line-strong);
852
+ border-radius: var(--radius-lg);
853
+ background: linear-gradient(180deg, #11141a 0%, #0f131b 100%);
854
+ }
855
+
856
+ cts-event-log:not(:defined) {
857
+ min-height: 240px;
858
+ border: 1px solid var(--line-strong);
859
+ border-radius: var(--radius-md);
860
+ background: linear-gradient(180deg, #11141a 0%, #0f131b 100%);
861
+ }
862
+
863
+ cts-counter:defined,
864
+ cts-todo-dashboard:defined,
865
+ cts-event-log:defined {
866
+ animation: cts-hydrate 280ms var(--ease) both;
867
+ }
868
+
869
+ /* ─────────────────────────────────────────────────────────────
870
+ Decorators / API at-a-glance
871
+ ───────────────────────────────────────────────────────────── */
872
+
873
+ .api {
874
+ padding: clamp(60px, 8vw, 96px) 0;
875
+ border-top: 1px solid var(--line);
876
+ position: relative;
877
+ }
878
+
879
+ .api__head {
880
+ display: flex;
881
+ align-items: end;
882
+ justify-content: space-between;
883
+ gap: 28px;
884
+ flex-wrap: wrap;
885
+ margin-bottom: 36px;
886
+ }
887
+ .api__title {
888
+ margin: 12px 0 0;
889
+ font-size: clamp(28px, 3.4vw, 38px);
890
+ font-weight: 600;
891
+ letter-spacing: -0.02em;
892
+ max-width: 18ch;
893
+ text-wrap: balance;
894
+ }
895
+ .api__lede {
896
+ max-width: 44ch;
897
+ color: var(--fg-muted);
898
+ font-size: 15px;
899
+ text-wrap: pretty;
900
+ margin: 0;
901
+ }
902
+
903
+ .api__grid {
904
+ display: grid;
905
+ gap: 14px;
906
+ grid-template-columns: 1fr;
907
+ }
908
+ @media (min-width: 640px) {
909
+ .api__grid { grid-template-columns: repeat(2, 1fr); }
910
+ }
911
+ @media (min-width: 980px) {
912
+ .api__grid { grid-template-columns: repeat(3, 1fr); }
913
+ }
914
+
915
+ .api-card {
916
+ position: relative;
917
+ padding: 22px;
918
+ border-radius: var(--radius-md);
919
+ border: 1px solid var(--line);
920
+ background: var(--bg-elev-1);
921
+ transition: border-color var(--dur) var(--ease),
922
+ background var(--dur) var(--ease),
923
+ transform var(--dur) var(--ease);
924
+ }
925
+ .api-card:hover {
926
+ border-color: var(--line-accent);
927
+ background: var(--bg-elev-2);
928
+ transform: translateY(-2px);
929
+ }
930
+ .api-card__name {
931
+ display: inline-flex;
932
+ align-items: center;
933
+ gap: 6px;
934
+ font-family: var(--font-mono);
935
+ font-size: 14px;
936
+ color: var(--accent-fg);
937
+ }
938
+ .api-card__name .target {
939
+ margin-left: 8px;
940
+ padding: 2px 8px;
941
+ border-radius: var(--radius-xs);
942
+ background: rgba(255, 255, 255, 0.04);
943
+ border: 1px solid var(--line);
944
+ color: var(--fg-subtle);
945
+ font-size: 11px;
946
+ letter-spacing: 0.04em;
947
+ text-transform: uppercase;
948
+ }
949
+ .api-card__desc {
950
+ margin: 10px 0 0;
951
+ font-size: 14px;
952
+ color: var(--fg-muted);
953
+ line-height: 1.55;
954
+ }
955
+
956
+ /* ─────────────────────────────────────────────────────────────
957
+ Footer
958
+ ───────────────────────────────────────────────────────────── */
959
+
960
+ .footer {
961
+ border-top: 1px solid var(--line);
962
+ padding: 28px 0 36px;
963
+ }
964
+ .footer__inner {
965
+ display: flex;
966
+ flex-wrap: wrap;
967
+ align-items: center;
968
+ justify-content: space-between;
969
+ gap: 16px;
970
+ font-family: var(--font-mono);
971
+ font-size: 12px;
972
+ color: var(--fg-subtle);
973
+ }
974
+ .footer a { color: var(--fg-muted); }
975
+ .footer a:hover { color: var(--accent); }
976
+ .footer__links {
977
+ display: inline-flex;
978
+ gap: 18px;
979
+ flex-wrap: wrap;
980
+ }
981
+
982
+ /* ─────────────────────────────────────────────────────────────
983
+ Loading skeletons
984
+ Shown until the JS bundle hydrates the custom elements.
985
+ :not(:defined) matches a custom element *before* it has been
986
+ registered via customElements.define() — so the skeleton state
987
+ evaporates the instant the bundle finishes executing.
988
+ ───────────────────────────────────────────────────────────── */
989
+
990
+ cts-code-example,
991
+ cts-message {
992
+ display: block;
993
+ }
994
+
995
+ cts-code-example:not(:defined),
996
+ cts-message:not(:defined) {
997
+ position: relative;
998
+ overflow: hidden;
999
+ }
1000
+
1001
+ /* Shared shimmer sweep — sits on top of the skeleton geometry */
1002
+ cts-code-example:not(:defined)::after,
1003
+ cts-message:not(:defined)::after {
1004
+ content: "";
1005
+ position: absolute;
1006
+ inset: 0;
1007
+ background-image: linear-gradient(
1008
+ 100deg,
1009
+ transparent 0%,
1010
+ rgba(255, 255, 255, 0.045) 50%,
1011
+ transparent 100%
1012
+ );
1013
+ background-size: 220% 100%;
1014
+ background-repeat: no-repeat;
1015
+ animation: cts-shimmer 1.5s ease-in-out infinite;
1016
+ pointer-events: none;
1017
+ }
1018
+
1019
+ /* Code skeleton lines via stacked gradients. */
1020
+ cts-code-example:not(:defined) {
1021
+ --skel: rgba(255, 255, 255, 0.06);
1022
+ min-height: 380px;
1023
+ padding: 28px 32px;
1024
+ background-color: transparent;
1025
+ background-origin: content-box;
1026
+ background-repeat: no-repeat;
1027
+ background-image:
1028
+ linear-gradient(var(--skel), var(--skel)),
1029
+ linear-gradient(var(--skel), var(--skel)),
1030
+ linear-gradient(var(--skel), var(--skel)),
1031
+ linear-gradient(var(--skel), var(--skel)),
1032
+ linear-gradient(var(--skel), var(--skel)),
1033
+ linear-gradient(var(--skel), var(--skel)),
1034
+ linear-gradient(var(--skel), var(--skel)),
1035
+ linear-gradient(var(--skel), var(--skel)),
1036
+ linear-gradient(var(--skel), var(--skel));
1037
+ background-size:
1038
+ 36% 9px,
1039
+ 72% 9px,
1040
+ 62% 9px,
1041
+ 78% 9px,
1042
+ 50% 9px,
1043
+ 56% 9px,
1044
+ 74% 9px,
1045
+ 44% 9px,
1046
+ 66% 9px;
1047
+ background-position:
1048
+ 0 0,
1049
+ 0 28px,
1050
+ 0 56px,
1051
+ 0 100px, /* visual gap (blank "line") */
1052
+ 0 128px,
1053
+ 0 156px,
1054
+ 0 200px, /* visual gap */
1055
+ 0 228px,
1056
+ 0 256px;
1057
+ }
1058
+
1059
+ @media (min-width: 980px) {
1060
+ cts-code-example:not(:defined) { min-height: 480px; }
1061
+ }
1062
+
1063
+ /* Install row: single shimmering bar matching the real row's box */
1064
+ cts-message:not(:defined) {
1065
+ height: 46px;
1066
+ margin: 18px 22px 6px;
1067
+ background: rgba(255, 255, 255, 0.025);
1068
+ border: 1px solid rgba(255, 255, 255, 0.08);
1069
+ border-radius: var(--radius-md);
1070
+ }
1071
+
1072
+ @keyframes cts-shimmer {
1073
+ 0% { background-position: 110% 0; }
1074
+ 100% { background-position: -110% 0; }
1075
+ }
1076
+
1077
+ /* Fade in once custom elements are defined. */
1078
+ cts-code-example:defined,
1079
+ cts-message:defined {
1080
+ animation: cts-hydrate 280ms var(--ease) both;
1081
+ }
1082
+
1083
+ @keyframes cts-hydrate {
1084
+ from { opacity: 0; }
1085
+ to { opacity: 1; }
1086
+ }
1087
+
1088
+ /* Mobile / narrow-viewport adjustments */
1089
+ @media (max-width: 720px) {
1090
+ .playground__head {
1091
+ flex-direction: column;
1092
+ align-items: stretch;
1093
+ gap: 14px;
1094
+ padding: 20px 18px 16px;
1095
+ }
1096
+ .playground__actions {
1097
+ justify-content: flex-start;
1098
+ flex-wrap: wrap;
1099
+ }
1100
+ .playground__title {
1101
+ font-size: 19px;
1102
+ }
1103
+ .playground__sub {
1104
+ font-size: 13.5px;
1105
+ }
1106
+
1107
+ .playground__body {
1108
+ padding: 18px 16px 22px;
1109
+ gap: 18px;
1110
+ }
1111
+
1112
+ .stage {
1113
+ padding: 22px 14px;
1114
+ min-height: 200px;
1115
+ }
1116
+ .stage--todo {
1117
+ padding: 22px 10px;
1118
+ }
1119
+
1120
+ cts-code-example {
1121
+ font-size: 12px;
1122
+ }
1123
+ }
1124
+
1125
+ /* ─────────────────────────────────────────────────────────────
1126
+ Reduced motion
1127
+ ───────────────────────────────────────────────────────────── */
1128
+
1129
+ @media (prefers-reduced-motion: reduce) {
1130
+ *, *::before, *::after {
1131
+ animation: none !important;
1132
+ transition: none !important;
1133
+ scroll-behavior: auto !important;
1134
+ }
1135
+ }