custom-elements-ts 0.1.0 → 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.
- package/.eslintrc.json +3 -2
- package/README.md +10 -3
- package/assets/readme-header.png +0 -0
- package/assets/social-preview.jpg +0 -0
- package/demos/site/event-log/event-log.element.ts +3 -14
- package/demos/site/index.html +39 -17
- package/demos/site/index.ts +6 -0
- package/demos/site/llms.txt +53 -0
- package/demos/site/og-image.png +0 -0
- package/demos/site/scroll-restoration.ts +28 -0
- package/demos/site/source-toggle/source-toggle.ts +88 -0
- package/demos/site/source-viewer/source-viewer.element.scss +292 -0
- package/demos/site/source-viewer/source-viewer.element.ts +138 -0
- package/demos/site/source-viewer/sources.generated.ts +11 -0
- package/demos/site/styles/site.css +144 -32
- package/demos/todo-dashboard/todo-dashboard.element.scss +8 -3
- package/demos/todo-dashboard/todo-dashboard.element.ts +1 -0
- package/demos/todo-dashboard/todo-item.element.ts +1 -0
- package/package.json +3 -3
- package/src/index.ts +3 -2
- package/src/signal.ts +66 -0
- package/src/state.ts +20 -8
- package/src/template-runtime.ts +404 -56
- package/tests/map-shallow-state.spec.ts +184 -0
- package/tests/signal.spec.ts +117 -0
- package/tools/build.js +14 -5
- package/tools/generate-sources.js +76 -0
- package/tools/rollup-config.js +2 -2
- package/tools/start.js +13 -7
package/.eslintrc.json
CHANGED
|
@@ -19,8 +19,9 @@
|
|
|
19
19
|
"parser": "espree",
|
|
20
20
|
"env": { "node": true, "es6": true },
|
|
21
21
|
"rules": {
|
|
22
|
-
"@typescript-eslint/no-var-requires": "off",
|
|
23
|
-
"@typescript-eslint/no-
|
|
22
|
+
"@typescript-eslint/no-var-requires": "off",
|
|
23
|
+
"@typescript-eslint/no-require-imports": "off",
|
|
24
|
+
"@typescript-eslint/no-unused-vars": "off",
|
|
24
25
|
"no-undef": "off",
|
|
25
26
|
"no-console": "off",
|
|
26
27
|
"prettier/prettier": "off"
|
package/README.md
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
# custom-elements-ts
|
|
2
2
|
|
|
3
|
-
[](https://coveralls.io/github/geocine/custom-elements-ts?branch=master)
|
|
4
|
-

|
|
5
|
-
[](https://www.npmjs.com/package/custom-elements-ts)
|
|
3
|
+
[](https://coveralls.io/github/geocine/custom-elements-ts?branch=master)
|
|
4
|
+

|
|
5
|
+
[](https://www.npmjs.com/package/custom-elements-ts)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://deepwiki.com/geocine/custom-elements-ts)
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://geocine.github.io/custom-elements-ts/">
|
|
11
|
+
<img src="assets/readme-header.png" alt="custom-elements-ts — Native Web Components, written in TypeScript">
|
|
12
|
+
</a>
|
|
13
|
+
</p>
|
|
7
14
|
|
|
8
15
|
Author native Web Components in TypeScript with a small set of decorators
|
|
9
16
|
(`@CustomElement`, `@Prop`, `@State`, `@Watch`, `@Listen`, `@Dispatch`,
|
|
Binary file
|
|
Binary file
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CustomElement,
|
|
3
|
-
Prop,
|
|
4
|
-
State,
|
|
5
|
-
TemplateResult,
|
|
6
|
-
Watch,
|
|
7
|
-
html,
|
|
8
|
-
} from 'custom-elements-ts';
|
|
1
|
+
import { CustomElement, Prop, State, TemplateResult, Watch, html } from 'custom-elements-ts';
|
|
9
2
|
|
|
10
3
|
interface LogEntry {
|
|
11
4
|
id: number;
|
|
@@ -17,11 +10,7 @@ interface LogEntry {
|
|
|
17
10
|
const MAX_ENTRIES = 6;
|
|
18
11
|
|
|
19
12
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* Subscribes to one or more bubbling, composed CustomEvents at the document
|
|
23
|
-
* level and renders the most recent few. Pure render() / @State() — no manual
|
|
24
|
-
* DOM diffing — so it doubles as a live example of the templating runtime.
|
|
13
|
+
* Live event log for bubbling CustomEvents on the showcase page.
|
|
25
14
|
*/
|
|
26
15
|
@CustomElement({
|
|
27
16
|
tag: 'cts-event-log',
|
|
@@ -111,7 +100,7 @@ export class EventLogElement extends HTMLElement {
|
|
|
111
100
|
}
|
|
112
101
|
|
|
113
102
|
private formatDetail(detail: unknown): string {
|
|
114
|
-
if (detail
|
|
103
|
+
if (detail === null || detail === undefined) return '—';
|
|
115
104
|
if (typeof detail === 'string') return detail;
|
|
116
105
|
if (typeof detail === 'number' || typeof detail === 'boolean') {
|
|
117
106
|
return String(detail);
|
package/demos/site/index.html
CHANGED
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
<section class="hero">
|
|
76
76
|
<div class="container">
|
|
77
77
|
<div class="hero__pill" role="status">
|
|
78
|
-
<span class="badge">v0.0
|
|
78
|
+
<span class="badge">v0.2.0</span>
|
|
79
79
|
<span>TypeScript 5</span>
|
|
80
80
|
<span class="sep">·</span>
|
|
81
81
|
<span>MIT</span>
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
<div class="container">
|
|
121
121
|
<div class="showcase__grid">
|
|
122
122
|
|
|
123
|
-
<article class="window" aria-label="
|
|
123
|
+
<article class="window" aria-label="message.element.ts source">
|
|
124
124
|
<header class="window__bar">
|
|
125
125
|
<span class="window__dots" aria-hidden="true">
|
|
126
126
|
<span></span><span></span><span></span>
|
|
@@ -128,10 +128,6 @@
|
|
|
128
128
|
<span class="window__title">
|
|
129
129
|
message.<span class="accent">element.ts</span>
|
|
130
130
|
</span>
|
|
131
|
-
<span class="window__copy">
|
|
132
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
133
|
-
what runs the install pill →
|
|
134
|
-
</span>
|
|
135
131
|
</header>
|
|
136
132
|
<div class="window__body">
|
|
137
133
|
<cts-code-example></cts-code-example>
|
|
@@ -212,10 +208,21 @@
|
|
|
212
208
|
whenever its <code>@State()</code> changes.
|
|
213
209
|
</p>
|
|
214
210
|
</div>
|
|
215
|
-
<
|
|
216
|
-
<
|
|
217
|
-
|
|
218
|
-
|
|
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>
|
|
219
226
|
</header>
|
|
220
227
|
|
|
221
228
|
<div class="playground__body playground__body--split">
|
|
@@ -235,6 +242,8 @@
|
|
|
235
242
|
empty="Click the counter to dispatch counter.change events."
|
|
236
243
|
></cts-event-log>
|
|
237
244
|
</div>
|
|
245
|
+
|
|
246
|
+
<cts-source-viewer id="src-counter" slug="counter"></cts-source-viewer>
|
|
238
247
|
</article>
|
|
239
248
|
|
|
240
249
|
<!-- Todo dashboard playground -->
|
|
@@ -250,24 +259,37 @@
|
|
|
250
259
|
parent re-renders.
|
|
251
260
|
</p>
|
|
252
261
|
</div>
|
|
253
|
-
<
|
|
254
|
-
<
|
|
255
|
-
|
|
256
|
-
|
|
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>
|
|
257
277
|
</header>
|
|
258
278
|
|
|
259
|
-
<div class="playground__body playground__body--
|
|
279
|
+
<div class="playground__body playground__body--split">
|
|
260
280
|
<div class="stage stage--todo">
|
|
261
|
-
<cts-todo-dashboard heading="Sprint board"></cts-todo-dashboard>
|
|
281
|
+
<cts-todo-dashboard heading="Sprint board" compact></cts-todo-dashboard>
|
|
262
282
|
</div>
|
|
263
283
|
|
|
264
284
|
<cts-event-log
|
|
265
|
-
class="playground__log
|
|
285
|
+
class="playground__log"
|
|
266
286
|
label="todo events"
|
|
267
287
|
events="todo.change,todo.toggle,todo.labelchange,todo.remove,filter.change,goal.change"
|
|
268
288
|
empty="Add, complete, edit, or remove a task to see events bubble up here."
|
|
269
289
|
></cts-event-log>
|
|
270
290
|
</div>
|
|
291
|
+
|
|
292
|
+
<cts-source-viewer id="src-todo" slug="todo-dashboard"></cts-source-viewer>
|
|
271
293
|
</article>
|
|
272
294
|
</div>
|
|
273
295
|
</section>
|
package/demos/site/index.ts
CHANGED
|
@@ -3,6 +3,12 @@ export * from './code-example/code-example.element';
|
|
|
3
3
|
export * from './message/message.element';
|
|
4
4
|
export * from './toast/toast.element';
|
|
5
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';
|
|
6
12
|
|
|
7
13
|
// Live demos hosted from sibling demo folders so visitors can interact
|
|
8
14
|
// with the real components, not screenshots.
|
|
@@ -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.
|
package/demos/site/og-image.png
CHANGED
|
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);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delegates [Source] button clicks to matching <cts-source-viewer> panels.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const TOGGLE_ATTR = 'data-source-toggle';
|
|
6
|
+
const TARGET_ATTR = 'data-source-target';
|
|
7
|
+
const OPEN_ATTR = 'open';
|
|
8
|
+
const ACTIVE_CLASS = 'is-active';
|
|
9
|
+
|
|
10
|
+
// Sticky-nav height plus breathing room.
|
|
11
|
+
const SCROLL_OFFSET = 88;
|
|
12
|
+
|
|
13
|
+
function prefersReducedMotion(): boolean {
|
|
14
|
+
return (
|
|
15
|
+
typeof window !== 'undefined' &&
|
|
16
|
+
typeof window.matchMedia === 'function' &&
|
|
17
|
+
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function scrollPanelIntoView(panel: HTMLElement) {
|
|
22
|
+
// Wait for [open] to land before measuring the panel.
|
|
23
|
+
requestAnimationFrame(() => {
|
|
24
|
+
const top = panel.getBoundingClientRect().top + window.scrollY - SCROLL_OFFSET;
|
|
25
|
+
if (top <= window.scrollY) return;
|
|
26
|
+
window.scrollTo({
|
|
27
|
+
top,
|
|
28
|
+
behavior: prefersReducedMotion() ? 'auto' : 'smooth',
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function findToggle(target: EventTarget | null): HTMLElement | null {
|
|
34
|
+
if (!(target instanceof Element)) return null;
|
|
35
|
+
return target.closest(`[${TOGGLE_ATTR}]`) as HTMLElement | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function syncTrigger(trigger: HTMLElement, isOpen: boolean) {
|
|
39
|
+
trigger.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
|
40
|
+
trigger.classList.toggle(ACTIVE_CLASS, isOpen);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function handleClick(event: MouseEvent) {
|
|
44
|
+
const trigger = findToggle(event.target);
|
|
45
|
+
if (!trigger) return;
|
|
46
|
+
|
|
47
|
+
const targetId = trigger.getAttribute(TARGET_ATTR);
|
|
48
|
+
if (!targetId) return;
|
|
49
|
+
|
|
50
|
+
const panel = document.getElementById(targetId);
|
|
51
|
+
if (!panel) return;
|
|
52
|
+
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
|
|
55
|
+
const willOpen = !panel.hasAttribute(OPEN_ATTR);
|
|
56
|
+
if (willOpen) {
|
|
57
|
+
panel.setAttribute(OPEN_ATTR, '');
|
|
58
|
+
} else {
|
|
59
|
+
panel.removeAttribute(OPEN_ATTR);
|
|
60
|
+
}
|
|
61
|
+
syncTrigger(trigger, willOpen);
|
|
62
|
+
|
|
63
|
+
// Closing should leave the user where they are.
|
|
64
|
+
if (willOpen) {
|
|
65
|
+
scrollPanelIntoView(panel);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function init() {
|
|
70
|
+
// Reflect any initially open panels.
|
|
71
|
+
document.querySelectorAll<HTMLElement>(`[${TOGGLE_ATTR}]`).forEach((trigger) => {
|
|
72
|
+
const targetId = trigger.getAttribute(TARGET_ATTR);
|
|
73
|
+
if (!targetId) return;
|
|
74
|
+
const panel = document.getElementById(targetId);
|
|
75
|
+
if (!panel) return;
|
|
76
|
+
syncTrigger(trigger, panel.hasAttribute(OPEN_ATTR));
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
document.addEventListener('click', handleClick);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (typeof document !== 'undefined') {
|
|
83
|
+
if (document.readyState === 'loading') {
|
|
84
|
+
document.addEventListener('DOMContentLoaded', init, { once: true });
|
|
85
|
+
} else {
|
|
86
|
+
init();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/* ─────────────────────────────────────────────────────────────
|
|
2
|
+
cts-source-viewer
|
|
3
|
+
Collapsible inline source panel for playground examples.
|
|
4
|
+
───────────────────────────────────────────────────────────── */
|
|
5
|
+
|
|
6
|
+
:host {
|
|
7
|
+
display: grid;
|
|
8
|
+
grid-template-rows: 0fr;
|
|
9
|
+
transition: grid-template-rows 360ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
10
|
+
font-family: "Geist Mono", "JetBrains Mono", ui-monospace, SFMono-Regular,
|
|
11
|
+
Menlo, Consolas, "Courier New", monospace;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
:host([open]) {
|
|
15
|
+
grid-template-rows: 1fr;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
* {
|
|
19
|
+
box-sizing: border-box;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.viewer {
|
|
23
|
+
overflow: hidden;
|
|
24
|
+
min-height: 0;
|
|
25
|
+
visibility: hidden;
|
|
26
|
+
pointer-events: none;
|
|
27
|
+
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
|
28
|
+
background:
|
|
29
|
+
radial-gradient(420px 200px at 0% 0%,
|
|
30
|
+
rgba(49, 120, 198, 0.06) 0%,
|
|
31
|
+
transparent 70%),
|
|
32
|
+
linear-gradient(180deg, #0d1118 0%, #0a0d13 100%);
|
|
33
|
+
transition: visibility 0s linear 360ms;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
:host([open]) .viewer {
|
|
37
|
+
visibility: visible;
|
|
38
|
+
pointer-events: auto;
|
|
39
|
+
transition-delay: 0s;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* ── Top bar: kicker + file tabs ─────────────────────────── */
|
|
43
|
+
|
|
44
|
+
.bar {
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: stretch;
|
|
47
|
+
gap: 4px;
|
|
48
|
+
padding: 0 14px;
|
|
49
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
|
50
|
+
background: linear-gradient(180deg,
|
|
51
|
+
rgba(255, 255, 255, 0.02) 0%,
|
|
52
|
+
rgba(255, 255, 255, 0) 100%);
|
|
53
|
+
overflow-x: auto;
|
|
54
|
+
scrollbar-width: none;
|
|
55
|
+
|
|
56
|
+
&::-webkit-scrollbar { display: none; }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.kicker {
|
|
60
|
+
display: inline-flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
gap: 8px;
|
|
63
|
+
padding: 0 14px 0 4px;
|
|
64
|
+
margin-right: 4px;
|
|
65
|
+
border-right: 1px solid rgba(255, 255, 255, 0.05);
|
|
66
|
+
font-size: 11px;
|
|
67
|
+
letter-spacing: 0.06em;
|
|
68
|
+
text-transform: uppercase;
|
|
69
|
+
color: rgba(255, 255, 255, 0.4);
|
|
70
|
+
white-space: nowrap;
|
|
71
|
+
align-self: center;
|
|
72
|
+
height: 30px;
|
|
73
|
+
}
|
|
74
|
+
.kicker-dot {
|
|
75
|
+
width: 6px;
|
|
76
|
+
height: 6px;
|
|
77
|
+
border-radius: 1px;
|
|
78
|
+
background: #4189d6;
|
|
79
|
+
box-shadow: 0 0 8px rgba(65, 137, 214, 0.6);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.tabs {
|
|
83
|
+
display: inline-flex;
|
|
84
|
+
align-items: stretch;
|
|
85
|
+
gap: 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.tab {
|
|
89
|
+
position: relative;
|
|
90
|
+
display: inline-flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
padding: 10px 12px;
|
|
93
|
+
background: transparent;
|
|
94
|
+
border: 0;
|
|
95
|
+
border-bottom: 2px solid transparent;
|
|
96
|
+
font-family: inherit;
|
|
97
|
+
font-size: 12px;
|
|
98
|
+
font-weight: 500;
|
|
99
|
+
color: rgba(255, 255, 255, 0.45);
|
|
100
|
+
cursor: pointer;
|
|
101
|
+
white-space: nowrap;
|
|
102
|
+
transition: color 150ms ease, border-color 150ms ease, background 150ms ease;
|
|
103
|
+
|
|
104
|
+
&:hover {
|
|
105
|
+
color: rgba(255, 255, 255, 0.82);
|
|
106
|
+
background: rgba(255, 255, 255, 0.02);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
&:focus-visible {
|
|
110
|
+
outline: 2px solid #4189d6;
|
|
111
|
+
outline-offset: -2px;
|
|
112
|
+
border-radius: 2px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
&.is-active {
|
|
116
|
+
color: #9cc0eb;
|
|
117
|
+
border-bottom-color: #4189d6;
|
|
118
|
+
background: rgba(65, 137, 214, 0.04);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.single {
|
|
123
|
+
display: inline-flex;
|
|
124
|
+
align-items: center;
|
|
125
|
+
padding: 10px 12px;
|
|
126
|
+
font-size: 12px;
|
|
127
|
+
font-weight: 500;
|
|
128
|
+
color: #9cc0eb;
|
|
129
|
+
white-space: nowrap;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.empty {
|
|
133
|
+
margin: 0;
|
|
134
|
+
padding: 16px 22px;
|
|
135
|
+
font-size: 12.5px;
|
|
136
|
+
color: rgba(255, 255, 255, 0.5);
|
|
137
|
+
}
|
|
138
|
+
.empty code {
|
|
139
|
+
padding: 1px 6px;
|
|
140
|
+
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
141
|
+
border-radius: 2px;
|
|
142
|
+
background: rgba(255, 255, 255, 0.025);
|
|
143
|
+
color: #9cc0eb;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* ── Code body ───────────────────────────────────────────── */
|
|
147
|
+
|
|
148
|
+
.body {
|
|
149
|
+
margin: 0;
|
|
150
|
+
padding: 22px 26px 26px;
|
|
151
|
+
background: transparent;
|
|
152
|
+
color: #d6dce5;
|
|
153
|
+
font-family: inherit;
|
|
154
|
+
font-size: 13px;
|
|
155
|
+
line-height: 1.65;
|
|
156
|
+
overflow: auto;
|
|
157
|
+
max-height: 460px;
|
|
158
|
+
white-space: pre;
|
|
159
|
+
-webkit-font-smoothing: antialiased;
|
|
160
|
+
|
|
161
|
+
scrollbar-width: thin;
|
|
162
|
+
scrollbar-color: rgba(255, 255, 255, 0.10) transparent;
|
|
163
|
+
|
|
164
|
+
&::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
165
|
+
&::-webkit-scrollbar-thumb {
|
|
166
|
+
background: rgba(255, 255, 255, 0.10);
|
|
167
|
+
border-radius: 2px;
|
|
168
|
+
}
|
|
169
|
+
&::-webkit-scrollbar-track { background: transparent; }
|
|
170
|
+
|
|
171
|
+
&:focus { outline: none; }
|
|
172
|
+
&:focus-visible {
|
|
173
|
+
outline: 2px solid #4189d6;
|
|
174
|
+
outline-offset: -2px;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.body code {
|
|
179
|
+
background: transparent;
|
|
180
|
+
font-family: inherit;
|
|
181
|
+
color: inherit;
|
|
182
|
+
font-size: inherit;
|
|
183
|
+
line-height: inherit;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@media (min-width: 980px) {
|
|
187
|
+
.body {
|
|
188
|
+
font-size: 13.5px;
|
|
189
|
+
padding: 26px 32px 30px;
|
|
190
|
+
max-height: 540px;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* Narrow viewports: preserve tab space and hint at overflow. */
|
|
195
|
+
@media (max-width: 640px) {
|
|
196
|
+
.bar {
|
|
197
|
+
padding: 0 10px;
|
|
198
|
+
mask-image: linear-gradient(to right,
|
|
199
|
+
black calc(100% - 22px),
|
|
200
|
+
transparent 100%);
|
|
201
|
+
-webkit-mask-image: linear-gradient(to right,
|
|
202
|
+
black calc(100% - 22px),
|
|
203
|
+
transparent 100%);
|
|
204
|
+
}
|
|
205
|
+
.kicker {
|
|
206
|
+
padding: 0 10px 0 4px;
|
|
207
|
+
margin-right: 2px;
|
|
208
|
+
font-size: 0; /* collapse text, keep the dot */
|
|
209
|
+
}
|
|
210
|
+
.tab,
|
|
211
|
+
.single {
|
|
212
|
+
padding: 10px 10px;
|
|
213
|
+
font-size: 11.5px;
|
|
214
|
+
}
|
|
215
|
+
.body {
|
|
216
|
+
padding: 18px 18px 22px;
|
|
217
|
+
font-size: 12.5px;
|
|
218
|
+
max-height: 380px;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* ── Prism token theme — kept in sync with cts-code-example ── */
|
|
223
|
+
|
|
224
|
+
.token {
|
|
225
|
+
&.comment,
|
|
226
|
+
&.prolog,
|
|
227
|
+
&.doctype,
|
|
228
|
+
&.cdata {
|
|
229
|
+
color: #6e7681;
|
|
230
|
+
font-style: italic;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
&.punctuation { color: #c9d1d9; }
|
|
234
|
+
|
|
235
|
+
&.property,
|
|
236
|
+
&.tag,
|
|
237
|
+
&.attr-name { color: #9cdcfe; }
|
|
238
|
+
|
|
239
|
+
&.keyword,
|
|
240
|
+
&.boolean { color: #4189d6; }
|
|
241
|
+
|
|
242
|
+
&.class-name { color: #4ec9b0; }
|
|
243
|
+
|
|
244
|
+
&.constant,
|
|
245
|
+
&.symbol { color: #4189d6; }
|
|
246
|
+
|
|
247
|
+
&.number { color: #b5cea8; }
|
|
248
|
+
|
|
249
|
+
&.string,
|
|
250
|
+
&.char,
|
|
251
|
+
&.attr-value,
|
|
252
|
+
&.builtin,
|
|
253
|
+
&.inserted { color: #ce9178; }
|
|
254
|
+
|
|
255
|
+
&.selector { color: #d7ba7d; }
|
|
256
|
+
|
|
257
|
+
&.variable { color: #d6dce5; }
|
|
258
|
+
|
|
259
|
+
&.operator,
|
|
260
|
+
&.entity { color: #d6dce5; }
|
|
261
|
+
|
|
262
|
+
&.url {
|
|
263
|
+
color: #4189d6;
|
|
264
|
+
text-decoration: underline;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
&.atrule { color: #c586c0; }
|
|
268
|
+
|
|
269
|
+
&.function { color: #dcdcaa; }
|
|
270
|
+
|
|
271
|
+
&.regex { color: #d16969; }
|
|
272
|
+
|
|
273
|
+
&.important {
|
|
274
|
+
color: #f47067;
|
|
275
|
+
font-weight: 600;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
&.bold { font-weight: 600; }
|
|
279
|
+
&.italic { font-style: italic; }
|
|
280
|
+
&.deleted { color: #f47067; }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.namespace { opacity: .65; }
|
|
284
|
+
|
|
285
|
+
.language-css .token.string,
|
|
286
|
+
.style .token.string {
|
|
287
|
+
color: #ce9178;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
@media (prefers-reduced-motion: reduce) {
|
|
291
|
+
:host { transition: none; }
|
|
292
|
+
}
|