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
@@ -0,0 +1,1023 @@
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
+ }
171
+ .brand__name .dim { color: var(--fg-subtle); }
172
+
173
+ .nav__links {
174
+ display: none;
175
+ align-items: center;
176
+ gap: 6px;
177
+ }
178
+ @media (min-width: 760px) {
179
+ .nav__links { display: inline-flex; }
180
+ }
181
+
182
+ .nav__link {
183
+ padding: 8px 12px;
184
+ border-radius: var(--radius-sm);
185
+ font-size: 14px;
186
+ color: var(--fg-muted);
187
+ }
188
+ .nav__link:hover {
189
+ color: var(--fg);
190
+ background: var(--bg-elev-1);
191
+ }
192
+
193
+ .nav__cta {
194
+ display: inline-flex;
195
+ align-items: center;
196
+ gap: 8px;
197
+ padding: 8px 12px;
198
+ border-radius: var(--radius-sm);
199
+ border: 1px solid var(--line-strong);
200
+ background: var(--bg-elev-1);
201
+ font-family: var(--font-mono);
202
+ font-size: 12px;
203
+ color: var(--fg);
204
+ transition: background var(--dur) var(--ease),
205
+ border-color var(--dur) var(--ease),
206
+ transform var(--dur) var(--ease);
207
+ }
208
+ .nav__cta:hover {
209
+ background: var(--bg-elev-2);
210
+ border-color: var(--line-accent);
211
+ color: var(--fg);
212
+ }
213
+ .nav__cta:active { transform: translateY(1px); }
214
+ .nav__cta svg { width: 14px; height: 14px; }
215
+ .nav__cta .stars {
216
+ color: var(--accent);
217
+ font-variant-numeric: tabular-nums;
218
+ }
219
+
220
+ /* ─────────────────────────────────────────────────────────────
221
+ Hero
222
+ ───────────────────────────────────────────────────────────── */
223
+
224
+ .hero {
225
+ padding: clamp(56px, 9vw, 110px) 0 clamp(40px, 6vw, 72px);
226
+ position: relative;
227
+ }
228
+
229
+ .hero__pill {
230
+ display: inline-flex;
231
+ align-items: center;
232
+ gap: 10px;
233
+ padding: 6px 12px 6px 8px;
234
+ border-radius: var(--radius-sm);
235
+ border: 1px solid var(--line-strong);
236
+ background: var(--bg-elev-1);
237
+ font-family: var(--font-mono);
238
+ font-size: 12px;
239
+ color: var(--fg-muted);
240
+ font-variant-numeric: tabular-nums;
241
+ }
242
+ .hero__pill .badge {
243
+ display: inline-flex;
244
+ align-items: center;
245
+ padding: 2px 8px;
246
+ border-radius: var(--radius-xs);
247
+ background: var(--accent-soft);
248
+ color: var(--accent-fg);
249
+ font-weight: 600;
250
+ letter-spacing: 0.02em;
251
+ }
252
+ .hero__pill .sep { color: var(--fg-subtle); }
253
+
254
+ .hero__title {
255
+ margin: 22px 0 18px;
256
+ font-size: clamp(40px, 6.4vw, 76px);
257
+ line-height: 1.02;
258
+ letter-spacing: -0.035em;
259
+ font-weight: 600;
260
+ color: var(--fg);
261
+ max-width: 14ch;
262
+ text-wrap: balance;
263
+ }
264
+ .hero__title .em {
265
+ font-style: normal;
266
+ color: var(--accent-fg);
267
+ }
268
+
269
+ .hero__lede {
270
+ margin: 0 0 32px;
271
+ max-width: 58ch;
272
+ font-size: clamp(16px, 1.4vw, 19px);
273
+ line-height: 1.55;
274
+ color: var(--fg-muted);
275
+ text-wrap: pretty;
276
+ }
277
+ .hero__lede strong {
278
+ color: var(--fg);
279
+ font-weight: 500;
280
+ }
281
+
282
+ .hero__actions {
283
+ display: flex;
284
+ flex-wrap: wrap;
285
+ gap: 12px;
286
+ margin-bottom: 28px;
287
+ }
288
+
289
+ .btn {
290
+ display: inline-flex;
291
+ align-items: center;
292
+ gap: 8px;
293
+ height: 42px;
294
+ padding: 0 18px;
295
+ border-radius: var(--radius-md);
296
+ font-family: var(--font-sans);
297
+ font-size: 14px;
298
+ font-weight: 500;
299
+ cursor: pointer;
300
+ border: 1px solid transparent;
301
+ transition: background var(--dur) var(--ease),
302
+ color var(--dur) var(--ease),
303
+ border-color var(--dur) var(--ease),
304
+ transform var(--dur-fast) var(--ease);
305
+ white-space: nowrap;
306
+ }
307
+ .btn:active { transform: translateY(1px); }
308
+
309
+ .btn--primary {
310
+ background: var(--accent);
311
+ color: #ffffff;
312
+ border-color: var(--accent);
313
+ box-shadow:
314
+ 0 1px 0 rgba(255, 255, 255, 0.18) inset,
315
+ 0 10px 24px -12px rgba(49, 120, 198, 0.65);
316
+ }
317
+ .btn--primary:hover {
318
+ background: var(--accent-bright);
319
+ color: #ffffff;
320
+ border-color: var(--accent-bright);
321
+ }
322
+
323
+ .btn--ghost {
324
+ background: transparent;
325
+ color: var(--fg);
326
+ border-color: var(--line-strong);
327
+ }
328
+ .btn--ghost:hover {
329
+ background: var(--bg-elev-1);
330
+ border-color: var(--line-accent);
331
+ color: var(--fg);
332
+ }
333
+
334
+ .btn .arrow {
335
+ width: 14px; height: 14px;
336
+ transition: transform var(--dur) var(--ease);
337
+ }
338
+ .btn:hover .arrow { transform: translateX(3px); }
339
+
340
+ .hero__meta {
341
+ display: flex;
342
+ flex-wrap: wrap;
343
+ gap: 22px;
344
+ font-family: var(--font-mono);
345
+ font-size: 12px;
346
+ color: var(--fg-subtle);
347
+ }
348
+ .hero__meta .ok { color: var(--ok); }
349
+
350
+ /* ─────────────────────────────────────────────────────────────
351
+ Showcase: code preview + key points (asymmetric)
352
+ ───────────────────────────────────────────────────────────── */
353
+
354
+ .showcase {
355
+ padding-bottom: clamp(60px, 8vw, 96px);
356
+ }
357
+
358
+ .showcase__grid {
359
+ display: grid;
360
+ gap: 24px;
361
+ grid-template-columns: 1fr;
362
+ }
363
+ @media (min-width: 980px) {
364
+ .showcase__grid {
365
+ grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr);
366
+ gap: 32px;
367
+ align-items: stretch;
368
+ }
369
+ }
370
+
371
+ /* Window-chrome wrapper around the code element */
372
+ .window {
373
+ position: relative;
374
+ border-radius: var(--radius-lg);
375
+ border: 1px solid var(--line-strong);
376
+ background: linear-gradient(180deg, #131720 0%, #0c0f15 100%);
377
+ overflow: hidden;
378
+ box-shadow:
379
+ 0 1px 0 rgba(255, 255, 255, 0.04) inset,
380
+ 0 30px 60px -30px rgba(0, 0, 0, 0.55);
381
+ }
382
+ .window__bar {
383
+ display: flex;
384
+ align-items: center;
385
+ gap: 10px;
386
+ padding: 10px 14px;
387
+ border-bottom: 1px solid var(--line);
388
+ background: linear-gradient(180deg, #181d27 0%, #11141a 100%);
389
+ }
390
+ .window__dots {
391
+ display: inline-flex;
392
+ gap: 6px;
393
+ }
394
+ .window__dots span {
395
+ width: 9px;
396
+ height: 9px;
397
+ border-radius: var(--radius-xs);
398
+ background: #2c313b;
399
+ border: 1px solid rgba(255, 255, 255, 0.04);
400
+ }
401
+ .window__dots span:nth-child(1) { background: #3a4150; }
402
+ .window__dots span:nth-child(2) { background: #2e3540; }
403
+ .window__dots span:nth-child(3) { background: #232932; }
404
+
405
+ .window__title {
406
+ margin-left: 8px;
407
+ font-family: var(--font-mono);
408
+ font-size: 12px;
409
+ color: var(--fg-subtle);
410
+ }
411
+ .window__title .accent { color: var(--accent-fg); }
412
+
413
+ .window__copy {
414
+ margin-left: auto;
415
+ display: inline-flex;
416
+ align-items: center;
417
+ gap: 6px;
418
+ padding: 4px 10px;
419
+ font-family: var(--font-mono);
420
+ font-size: 11px;
421
+ color: var(--fg-muted);
422
+ border: 1px solid var(--line);
423
+ border-radius: var(--radius-xs);
424
+ background: var(--bg-elev-2);
425
+ }
426
+ .window__copy svg { width: 12px; height: 12px; }
427
+
428
+ .window__body {
429
+ background: #0b0e14;
430
+ }
431
+
432
+ /* Side panel: install + checks + CTA */
433
+ .side-card {
434
+ display: flex;
435
+ flex-direction: column;
436
+ border-radius: var(--radius-lg);
437
+ border: 1px solid var(--line-strong);
438
+ background:
439
+ radial-gradient(180px 120px at 100% 0%,
440
+ rgba(49, 120, 198, 0.14) 0%,
441
+ transparent 70%),
442
+ linear-gradient(180deg, #131720 0%, #0c0f15 100%);
443
+ overflow: hidden;
444
+ }
445
+
446
+ .side-card__head {
447
+ padding: 22px 22px 6px;
448
+ }
449
+ .side-card__kicker {
450
+ font-family: var(--font-mono);
451
+ font-size: 11px;
452
+ letter-spacing: 0.06em;
453
+ text-transform: uppercase;
454
+ color: var(--accent-fg);
455
+ }
456
+ .side-card__title {
457
+ margin: 6px 0 0;
458
+ font-size: 22px;
459
+ font-weight: 600;
460
+ letter-spacing: -0.01em;
461
+ color: var(--fg);
462
+ }
463
+ .side-card__sub {
464
+ margin: 10px 0 16px;
465
+ color: var(--fg-muted);
466
+ font-size: 13.5px;
467
+ line-height: 1.55;
468
+ text-wrap: pretty;
469
+ }
470
+ .side-card__sub em {
471
+ font-style: normal;
472
+ color: var(--fg);
473
+ }
474
+ .side-card__sub code {
475
+ padding: 1px 6px;
476
+ border: 1px solid var(--line);
477
+ border-radius: var(--radius-xs);
478
+ background: rgba(255, 255, 255, 0.025);
479
+ font-family: var(--font-mono);
480
+ font-size: 12px;
481
+ color: var(--accent-fg);
482
+ }
483
+
484
+ /* Checklist */
485
+ .checks {
486
+ list-style: none;
487
+ margin: 0;
488
+ padding: 18px 22px 6px;
489
+ display: grid;
490
+ gap: 12px;
491
+ }
492
+ .checks li {
493
+ display: flex;
494
+ align-items: flex-start;
495
+ gap: 12px;
496
+ font-size: 14px;
497
+ color: var(--fg-muted);
498
+ }
499
+ .checks li strong {
500
+ color: var(--fg);
501
+ font-weight: 500;
502
+ }
503
+ .checks .check {
504
+ flex: 0 0 auto;
505
+ width: 18px;
506
+ height: 18px;
507
+ margin-top: 2px;
508
+ border-radius: var(--radius-xs);
509
+ background: var(--accent-soft);
510
+ color: var(--accent-fg);
511
+ display: grid;
512
+ place-items: center;
513
+ }
514
+ .checks .check svg { width: 11px; height: 11px; }
515
+
516
+ .side-card__foot {
517
+ margin-top: auto;
518
+ padding: 18px 22px 22px;
519
+ border-top: 1px solid var(--line);
520
+ background: rgba(255, 255, 255, 0.015);
521
+ }
522
+ .side-card__foot a {
523
+ display: inline-flex;
524
+ align-items: center;
525
+ gap: 8px;
526
+ font-family: var(--font-mono);
527
+ font-size: 13px;
528
+ color: var(--fg);
529
+ }
530
+ .side-card__foot a .arrow { transition: transform var(--dur) var(--ease); }
531
+ .side-card__foot a:hover .arrow { transform: translateX(3px); }
532
+
533
+ /* ─────────────────────────────────────────────────────────────
534
+ Live demos
535
+ Two playgrounds (counter, todo dashboard) hosted alongside
536
+ real <cts-event-log> panels so visitors can watch the events
537
+ the components emit in real time.
538
+ ───────────────────────────────────────────────────────────── */
539
+
540
+ .live {
541
+ padding: clamp(60px, 8vw, 96px) 0 clamp(40px, 6vw, 72px);
542
+ border-top: 1px solid var(--line);
543
+ position: relative;
544
+ }
545
+
546
+ .live__head {
547
+ display: flex;
548
+ align-items: end;
549
+ justify-content: space-between;
550
+ gap: 28px;
551
+ flex-wrap: wrap;
552
+ margin-bottom: 36px;
553
+ }
554
+ .live__title {
555
+ margin: 12px 0 0;
556
+ font-size: clamp(28px, 3.4vw, 38px);
557
+ font-weight: 600;
558
+ letter-spacing: -0.02em;
559
+ max-width: 22ch;
560
+ text-wrap: balance;
561
+ }
562
+ .live__lede {
563
+ max-width: 50ch;
564
+ color: var(--fg-muted);
565
+ font-size: 15px;
566
+ text-wrap: pretty;
567
+ margin: 0;
568
+ }
569
+ .live__lede code {
570
+ padding: 1px 6px;
571
+ border: 1px solid var(--line);
572
+ border-radius: var(--radius-xs);
573
+ background: rgba(255, 255, 255, 0.025);
574
+ font-family: var(--font-mono);
575
+ font-size: 12.5px;
576
+ color: var(--accent-fg);
577
+ }
578
+
579
+ .playground {
580
+ position: relative;
581
+ margin-top: 22px;
582
+ border-radius: var(--radius-lg);
583
+ border: 1px solid var(--line-strong);
584
+ background:
585
+ radial-gradient(160px 110px at 100% 0%,
586
+ rgba(49, 120, 198, 0.10) 0%,
587
+ transparent 70%),
588
+ linear-gradient(180deg, #131720 0%, #0c0f15 100%);
589
+ overflow: hidden;
590
+ box-shadow:
591
+ 0 1px 0 rgba(255, 255, 255, 0.04) inset,
592
+ 0 30px 60px -30px rgba(0, 0, 0, 0.55);
593
+ }
594
+
595
+ .playground__head {
596
+ display: flex;
597
+ align-items: flex-start;
598
+ justify-content: space-between;
599
+ gap: 18px;
600
+ padding: 22px 24px 18px;
601
+ border-bottom: 1px solid var(--line);
602
+ }
603
+ .playground__head > div {
604
+ min-width: 0;
605
+ }
606
+ .playground__kicker {
607
+ display: inline-block;
608
+ font-family: var(--font-mono);
609
+ font-size: 11px;
610
+ letter-spacing: 0.14em;
611
+ text-transform: uppercase;
612
+ color: var(--accent-fg);
613
+ }
614
+ .playground__title {
615
+ margin: 8px 0 0;
616
+ font-size: 20px;
617
+ font-weight: 600;
618
+ letter-spacing: -0.015em;
619
+ color: var(--fg);
620
+ text-wrap: balance;
621
+ }
622
+ .playground__title code {
623
+ padding: 1px 6px;
624
+ border: 1px solid var(--line);
625
+ border-radius: var(--radius-xs);
626
+ background: rgba(255, 255, 255, 0.025);
627
+ font-family: var(--font-mono);
628
+ font-size: 13px;
629
+ font-weight: 500;
630
+ color: var(--accent-fg);
631
+ }
632
+ .playground__sub {
633
+ margin: 10px 0 0;
634
+ max-width: 60ch;
635
+ color: var(--fg-muted);
636
+ font-size: 14px;
637
+ line-height: 1.6;
638
+ text-wrap: pretty;
639
+ }
640
+ .playground__sub code {
641
+ padding: 1px 5px;
642
+ border: 1px solid var(--line);
643
+ border-radius: var(--radius-xs);
644
+ background: rgba(255, 255, 255, 0.025);
645
+ font-family: var(--font-mono);
646
+ font-size: 12px;
647
+ color: var(--accent-fg);
648
+ }
649
+ .playground__source {
650
+ flex: 0 0 auto;
651
+ display: inline-flex;
652
+ align-items: center;
653
+ gap: 8px;
654
+ padding: 8px 12px;
655
+ border: 1px solid var(--line-strong);
656
+ border-radius: var(--radius-xs);
657
+ background: rgba(255, 255, 255, 0.02);
658
+ font-family: var(--font-mono);
659
+ font-size: 12px;
660
+ color: var(--fg-muted);
661
+ white-space: nowrap;
662
+ transition: border-color var(--dur) var(--ease),
663
+ color var(--dur) var(--ease),
664
+ background var(--dur) var(--ease);
665
+ }
666
+ .playground__source:hover {
667
+ color: var(--fg);
668
+ border-color: var(--line-accent);
669
+ background: var(--accent-soft);
670
+ }
671
+
672
+ .playground__body {
673
+ display: grid;
674
+ gap: 20px;
675
+ padding: 26px 24px 28px;
676
+ }
677
+
678
+ /* Counter playground: stage on the left, log on the right at >=900px. */
679
+ .playground__body--split {
680
+ grid-template-columns: 1fr;
681
+ }
682
+ @media (min-width: 900px) {
683
+ .playground__body--split {
684
+ grid-template-columns: minmax(0, 1fr) minmax(280px, 360px);
685
+ align-items: stretch;
686
+ }
687
+ }
688
+
689
+ /* Todo playground: dashboard on top, full-width log below.
690
+ The dashboard sets its own max width; we just center it. */
691
+ .playground__body--stack {
692
+ grid-template-columns: 1fr;
693
+ }
694
+
695
+ .stage {
696
+ display: grid;
697
+ place-items: center;
698
+ gap: 18px;
699
+ padding: 28px 18px;
700
+ border: 1px solid var(--line);
701
+ border-radius: var(--radius-md);
702
+ background:
703
+ radial-gradient(60% 60% at 50% 0%,
704
+ rgba(49, 120, 198, 0.05) 0%,
705
+ transparent 60%),
706
+ rgba(0, 0, 0, 0.18);
707
+ background-image:
708
+ linear-gradient(to right, rgba(255, 255, 255, 0.022) 1px, transparent 1px),
709
+ linear-gradient(to bottom, rgba(255, 255, 255, 0.022) 1px, transparent 1px),
710
+ radial-gradient(60% 60% at 50% 0%,
711
+ rgba(49, 120, 198, 0.05) 0%,
712
+ transparent 60%),
713
+ linear-gradient(180deg, #0d1118 0%, #0a0d13 100%);
714
+ background-size: 28px 28px, 28px 28px, auto, auto;
715
+ min-height: 240px;
716
+ }
717
+
718
+ .stage--counter {
719
+ grid-template-rows: auto auto;
720
+ }
721
+
722
+ .stage--todo {
723
+ padding: 32px 18px;
724
+ }
725
+
726
+ .stage__caption {
727
+ margin: 0;
728
+ max-width: 38ch;
729
+ color: var(--fg-subtle);
730
+ font-size: 12.5px;
731
+ line-height: 1.55;
732
+ text-align: center;
733
+ text-wrap: balance;
734
+ }
735
+ .stage__caption code {
736
+ padding: 1px 5px;
737
+ border: 1px solid var(--line);
738
+ border-radius: var(--radius-xs);
739
+ background: rgba(255, 255, 255, 0.02);
740
+ font-family: var(--font-mono);
741
+ font-size: 11.5px;
742
+ color: var(--accent-fg);
743
+ }
744
+
745
+ .playground__log {
746
+ display: flex; /* lets the host stretch in grid contexts */
747
+ }
748
+ .playground__log--wide {
749
+ /* Sits below the dashboard; let it span the full row. */
750
+ width: 100%;
751
+ }
752
+
753
+ /* Custom element loading skeletons for the live demos.
754
+ Match the same hydration pattern used on the install pill above. */
755
+ cts-counter,
756
+ cts-todo-dashboard,
757
+ cts-event-log {
758
+ display: block;
759
+ }
760
+
761
+ cts-counter:not(:defined) {
762
+ width: 320px;
763
+ height: 230px;
764
+ border: 1px solid var(--line-strong);
765
+ border-radius: var(--radius-lg);
766
+ background: linear-gradient(180deg, #11141a 0%, #0f131b 100%);
767
+ }
768
+
769
+ cts-todo-dashboard:not(:defined) {
770
+ width: min(720px, calc(100vw - 80px));
771
+ height: 520px;
772
+ border: 1px solid var(--line-strong);
773
+ border-radius: var(--radius-lg);
774
+ background: linear-gradient(180deg, #11141a 0%, #0f131b 100%);
775
+ }
776
+
777
+ cts-event-log:not(:defined) {
778
+ min-height: 240px;
779
+ border: 1px solid var(--line-strong);
780
+ border-radius: var(--radius-md);
781
+ background: linear-gradient(180deg, #11141a 0%, #0f131b 100%);
782
+ }
783
+
784
+ cts-counter:defined,
785
+ cts-todo-dashboard:defined,
786
+ cts-event-log:defined {
787
+ animation: cts-hydrate 280ms var(--ease) both;
788
+ }
789
+
790
+ /* ─────────────────────────────────────────────────────────────
791
+ Decorators / API at-a-glance
792
+ ───────────────────────────────────────────────────────────── */
793
+
794
+ .api {
795
+ padding: clamp(60px, 8vw, 96px) 0;
796
+ border-top: 1px solid var(--line);
797
+ position: relative;
798
+ }
799
+
800
+ .api__head {
801
+ display: flex;
802
+ align-items: end;
803
+ justify-content: space-between;
804
+ gap: 28px;
805
+ flex-wrap: wrap;
806
+ margin-bottom: 36px;
807
+ }
808
+ .api__title {
809
+ margin: 12px 0 0;
810
+ font-size: clamp(28px, 3.4vw, 38px);
811
+ font-weight: 600;
812
+ letter-spacing: -0.02em;
813
+ max-width: 18ch;
814
+ text-wrap: balance;
815
+ }
816
+ .api__lede {
817
+ max-width: 44ch;
818
+ color: var(--fg-muted);
819
+ font-size: 15px;
820
+ text-wrap: pretty;
821
+ margin: 0;
822
+ }
823
+
824
+ .api__grid {
825
+ display: grid;
826
+ gap: 14px;
827
+ grid-template-columns: 1fr;
828
+ }
829
+ @media (min-width: 640px) {
830
+ .api__grid { grid-template-columns: repeat(2, 1fr); }
831
+ }
832
+ @media (min-width: 980px) {
833
+ .api__grid { grid-template-columns: repeat(3, 1fr); }
834
+ }
835
+
836
+ .api-card {
837
+ position: relative;
838
+ padding: 22px;
839
+ border-radius: var(--radius-md);
840
+ border: 1px solid var(--line);
841
+ background: var(--bg-elev-1);
842
+ transition: border-color var(--dur) var(--ease),
843
+ background var(--dur) var(--ease),
844
+ transform var(--dur) var(--ease);
845
+ }
846
+ .api-card:hover {
847
+ border-color: var(--line-accent);
848
+ background: var(--bg-elev-2);
849
+ transform: translateY(-2px);
850
+ }
851
+ .api-card__name {
852
+ display: inline-flex;
853
+ align-items: center;
854
+ gap: 6px;
855
+ font-family: var(--font-mono);
856
+ font-size: 14px;
857
+ color: var(--accent-fg);
858
+ }
859
+ .api-card__name .target {
860
+ margin-left: 8px;
861
+ padding: 2px 8px;
862
+ border-radius: var(--radius-xs);
863
+ background: rgba(255, 255, 255, 0.04);
864
+ border: 1px solid var(--line);
865
+ color: var(--fg-subtle);
866
+ font-size: 11px;
867
+ letter-spacing: 0.04em;
868
+ text-transform: uppercase;
869
+ }
870
+ .api-card__desc {
871
+ margin: 10px 0 0;
872
+ font-size: 14px;
873
+ color: var(--fg-muted);
874
+ line-height: 1.55;
875
+ }
876
+
877
+ /* ─────────────────────────────────────────────────────────────
878
+ Footer
879
+ ───────────────────────────────────────────────────────────── */
880
+
881
+ .footer {
882
+ border-top: 1px solid var(--line);
883
+ padding: 28px 0 36px;
884
+ }
885
+ .footer__inner {
886
+ display: flex;
887
+ flex-wrap: wrap;
888
+ align-items: center;
889
+ justify-content: space-between;
890
+ gap: 16px;
891
+ font-family: var(--font-mono);
892
+ font-size: 12px;
893
+ color: var(--fg-subtle);
894
+ }
895
+ .footer a { color: var(--fg-muted); }
896
+ .footer a:hover { color: var(--accent); }
897
+ .footer__links {
898
+ display: inline-flex;
899
+ gap: 18px;
900
+ flex-wrap: wrap;
901
+ }
902
+
903
+ /* ─────────────────────────────────────────────────────────────
904
+ Loading skeletons
905
+ Shown until the JS bundle hydrates the custom elements.
906
+ :not(:defined) matches a custom element *before* it has been
907
+ registered via customElements.define() — so the skeleton state
908
+ evaporates the instant the bundle finishes executing.
909
+ ───────────────────────────────────────────────────────────── */
910
+
911
+ cts-code-example,
912
+ cts-message {
913
+ display: block;
914
+ }
915
+
916
+ cts-code-example:not(:defined),
917
+ cts-message:not(:defined) {
918
+ position: relative;
919
+ overflow: hidden;
920
+ }
921
+
922
+ /* Shared shimmer sweep — sits on top of the skeleton geometry */
923
+ cts-code-example:not(:defined)::after,
924
+ cts-message:not(:defined)::after {
925
+ content: "";
926
+ position: absolute;
927
+ inset: 0;
928
+ background-image: linear-gradient(
929
+ 100deg,
930
+ transparent 0%,
931
+ rgba(255, 255, 255, 0.045) 50%,
932
+ transparent 100%
933
+ );
934
+ background-size: 220% 100%;
935
+ background-repeat: no-repeat;
936
+ animation: cts-shimmer 1.5s ease-in-out infinite;
937
+ pointer-events: none;
938
+ }
939
+
940
+ /* Code window: fake "lines of code" via stacked gradients.
941
+ --skel is the line color; positions are content-box relative
942
+ so we don't have to add the padding offset to every value. */
943
+ cts-code-example:not(:defined) {
944
+ --skel: rgba(255, 255, 255, 0.06);
945
+ min-height: 380px;
946
+ padding: 28px 32px;
947
+ background-color: transparent;
948
+ background-origin: content-box;
949
+ background-repeat: no-repeat;
950
+ background-image:
951
+ linear-gradient(var(--skel), var(--skel)),
952
+ linear-gradient(var(--skel), var(--skel)),
953
+ linear-gradient(var(--skel), var(--skel)),
954
+ linear-gradient(var(--skel), var(--skel)),
955
+ linear-gradient(var(--skel), var(--skel)),
956
+ linear-gradient(var(--skel), var(--skel)),
957
+ linear-gradient(var(--skel), var(--skel)),
958
+ linear-gradient(var(--skel), var(--skel)),
959
+ linear-gradient(var(--skel), var(--skel));
960
+ background-size:
961
+ 36% 9px,
962
+ 72% 9px,
963
+ 62% 9px,
964
+ 78% 9px,
965
+ 50% 9px,
966
+ 56% 9px,
967
+ 74% 9px,
968
+ 44% 9px,
969
+ 66% 9px;
970
+ background-position:
971
+ 0 0,
972
+ 0 28px,
973
+ 0 56px,
974
+ 0 100px, /* visual gap (blank "line") */
975
+ 0 128px,
976
+ 0 156px,
977
+ 0 200px, /* visual gap */
978
+ 0 228px,
979
+ 0 256px;
980
+ }
981
+
982
+ @media (min-width: 980px) {
983
+ cts-code-example:not(:defined) { min-height: 480px; }
984
+ }
985
+
986
+ /* Install row: single shimmering bar matching the real row's box */
987
+ cts-message:not(:defined) {
988
+ height: 46px;
989
+ margin: 18px 22px 6px;
990
+ background: rgba(255, 255, 255, 0.025);
991
+ border: 1px solid rgba(255, 255, 255, 0.08);
992
+ border-radius: var(--radius-md);
993
+ }
994
+
995
+ @keyframes cts-shimmer {
996
+ 0% { background-position: 110% 0; }
997
+ 100% { background-position: -110% 0; }
998
+ }
999
+
1000
+ /* Soft fade-in once the real component swaps in, so the skeleton
1001
+ doesn't pop. `:defined` matches the moment customElements.define
1002
+ completes, which is exactly when the new content renders. */
1003
+ cts-code-example:defined,
1004
+ cts-message:defined {
1005
+ animation: cts-hydrate 280ms var(--ease) both;
1006
+ }
1007
+
1008
+ @keyframes cts-hydrate {
1009
+ from { opacity: 0; }
1010
+ to { opacity: 1; }
1011
+ }
1012
+
1013
+ /* ─────────────────────────────────────────────────────────────
1014
+ Reduced motion
1015
+ ───────────────────────────────────────────────────────────── */
1016
+
1017
+ @media (prefers-reduced-motion: reduce) {
1018
+ *, *::before, *::after {
1019
+ animation: none !important;
1020
+ transition: none !important;
1021
+ scroll-behavior: auto !important;
1022
+ }
1023
+ }