@ecopages/radiant 0.3.0-alpha.18 → 0.3.0-alpha.19
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/README.md +315 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# Radiant
|
|
2
|
+
|
|
3
|
+
Radiant is a light-DOM platform for custom elements and DOM-attached controllers.
|
|
4
|
+
|
|
5
|
+
It keeps browser primitives visible instead of wrapping them in a synthetic component model. You work with real custom elements, real DOM events, real attributes, and real light-DOM children. Use `RadiantElement` when you want reactive fields, JSX-backed rendering, SSR host serialization, and hydration on a real custom element. Use `RadiantController` when you want behavior attached to existing DOM instead of defining a custom element.
|
|
6
|
+
|
|
7
|
+
Radiant deliberately does not use shadow DOM by default. That makes styling, DOM inspection, and authored child content simpler, while giving up some of the encapsulation that conventional custom-element guidance usually prefers.
|
|
8
|
+
|
|
9
|
+
For the full docs site, see [radiant.ecopages.app](https://radiant.ecopages.app/).
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
`@ecopages/radiant` is the core package. `@ecopages/jsx` and `@ecopages/signals` are companion packages for TSX rendering and standalone signals.
|
|
14
|
+
|
|
15
|
+
`@ecopages/radiant` currently declares both companion packages as peer dependencies.
|
|
16
|
+
|
|
17
|
+
Install all three packages:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
bun add @ecopages/radiant @ecopages/signals @ecopages/jsx
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Application code does not need to import JSX helpers or Signals primitives directly for every feature, but the current published Radiant surface expects both companion packages to be installed alongside it.
|
|
24
|
+
|
|
25
|
+
## RadiantElement Mental Model
|
|
26
|
+
|
|
27
|
+
`RadiantElement` is the structured base class for reactive custom elements.
|
|
28
|
+
|
|
29
|
+
- `render()` returns the current JSX view.
|
|
30
|
+
- First connect automatically chooses between hydration and a fresh client render.
|
|
31
|
+
- `update()` reruns `render()` and commits the current view into the host immediately.
|
|
32
|
+
- `requestUpdate()` schedules one rerender in a microtask and coalesces repeated requests.
|
|
33
|
+
- `@prop(...)`, `@state`, and `@signal(...)` define reactive members.
|
|
34
|
+
- `@onUpdated(...)` is the bridge from reactive member changes to `update()` or `requestUpdate()` when the rendered structure needs to be recomputed.
|
|
35
|
+
- `this.bindings.key`, `this.$.key`, and `this.bind('key')` expose stable JSX bindings for reactive members.
|
|
36
|
+
- If `render()` is omitted, the base implementation behaves like `<slot />`, so authored light-DOM children pass through unchanged.
|
|
37
|
+
- Literal `<slot>` tags project authored light-DOM content, and `getSlotElement(...)`, `getSlotElements(...)`, or `@querySlot(...)` let component logic read the assigned elements.
|
|
38
|
+
|
|
39
|
+
The key distinction is this:
|
|
40
|
+
|
|
41
|
+
- Use bindings such as `this.$.count` when the shape of the view stays the same and only child values need to change.
|
|
42
|
+
- Use `update()` or `requestUpdate()` when a reactive change affects the structure of the JSX tree.
|
|
43
|
+
|
|
44
|
+
Signal and store reads performed directly inside `render()` also participate in rerender invalidation, so shared reactive data can drive `RadiantElement` views without an extra wrapper layer.
|
|
45
|
+
|
|
46
|
+
## Counter Example
|
|
47
|
+
|
|
48
|
+
This counter shows the intended `RadiantElement` style for stable templates: public props stay explicit, internal state stays local, and bound child values update without forcing a full rerender of the whole template.
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
/** @jsxImportSource @ecopages/jsx */
|
|
52
|
+
|
|
53
|
+
import type { JsxCustomElementAttributes } from '@ecopages/jsx';
|
|
54
|
+
import { RadiantElement, customElement, prop, state } from '@ecopages/radiant';
|
|
55
|
+
|
|
56
|
+
type CounterCardBindings = {
|
|
57
|
+
count: number;
|
|
58
|
+
label: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
type CounterCardAttributes = {
|
|
62
|
+
label?: string;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
@customElement('counter-card')
|
|
66
|
+
export class CounterCard extends RadiantElement<CounterCardBindings> {
|
|
67
|
+
@prop({ type: String, defaultValue: 'Clicks' }) label!: string;
|
|
68
|
+
@state count = 0;
|
|
69
|
+
|
|
70
|
+
private readonly increment = () => {
|
|
71
|
+
this.count += 1;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
override render() {
|
|
75
|
+
return (
|
|
76
|
+
<section>
|
|
77
|
+
<h2>{this.$.label}</h2>
|
|
78
|
+
<p>Count: {this.$.count}</p>
|
|
79
|
+
<button type="button" on:click={this.increment}>
|
|
80
|
+
Increment
|
|
81
|
+
</button>
|
|
82
|
+
</section>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
declare module '@ecopages/jsx/jsx-runtime' {
|
|
88
|
+
interface JsxCustomIntrinsicElements {
|
|
89
|
+
'counter-card': JsxCustomElementAttributes<HTMLElement, CounterCardAttributes>;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
How this works:
|
|
95
|
+
|
|
96
|
+
- `@prop(...)` makes `label` part of the component's public reactive property and attribute surface.
|
|
97
|
+
- `@state` is internal mutable state.
|
|
98
|
+
- `this.$.label` and `this.$.count` are cached JSX bindings, so those text positions can update directly when the underlying reactive members change.
|
|
99
|
+
- This example does not call `update()` because the view structure is stable. Only the bound child values change.
|
|
100
|
+
- The JSX intrinsic-element declaration uses a public attribute type that is narrower than the internal binding shape. That is usually the right split.
|
|
101
|
+
|
|
102
|
+
If the same type truly represents both internal bindings and public JSX attributes, you can reuse it for both. Otherwise, keep those contracts separate.
|
|
103
|
+
|
|
104
|
+
### When To Call `update()`
|
|
105
|
+
|
|
106
|
+
Call `update()` or `requestUpdate()` when a reactive change affects which elements should exist, not just their child values.
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { RadiantElement, onUpdated, state } from '@ecopages/radiant';
|
|
110
|
+
|
|
111
|
+
export class ResultsPanel extends RadiantElement<{ expanded: boolean }> {
|
|
112
|
+
@state expanded = false;
|
|
113
|
+
|
|
114
|
+
@onUpdated('expanded')
|
|
115
|
+
protected rerenderView(): void {
|
|
116
|
+
this.requestUpdate();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Use `update()` when you want the rerender immediately. Use `requestUpdate()` when several changes may happen in the same turn and one final rerender is enough.
|
|
122
|
+
|
|
123
|
+
## Reactive JSX Bindings
|
|
124
|
+
|
|
125
|
+
Bindings are the non-obvious part of the API, and they are worth being explicit about.
|
|
126
|
+
|
|
127
|
+
`RadiantElement<Bindings>` takes a dedicated binding shape, not the full class type. That keeps the binding namespace limited to the reactive props or fields you want JSX to consume.
|
|
128
|
+
|
|
129
|
+
These three forms are equivalent:
|
|
130
|
+
|
|
131
|
+
- `this.bindings.count`
|
|
132
|
+
- `this.$.count`
|
|
133
|
+
- `this.bind('count')`
|
|
134
|
+
|
|
135
|
+
They all resolve through the same cached binding object.
|
|
136
|
+
|
|
137
|
+
Use bindings when:
|
|
138
|
+
|
|
139
|
+
- the rendered element structure stays the same
|
|
140
|
+
- only text, attribute, or child-value positions need to change
|
|
141
|
+
- you want the renderer to patch the owned child range instead of rerunning the whole component view
|
|
142
|
+
|
|
143
|
+
Use `@onUpdated(...)` with `update()` or `requestUpdate()` when the reactive change should rebuild the JSX tree.
|
|
144
|
+
|
|
145
|
+
## SSR And Hydration
|
|
146
|
+
|
|
147
|
+
`RadiantElement` has two SSR surfaces:
|
|
148
|
+
|
|
149
|
+
- `renderToString()` serializes the component view only.
|
|
150
|
+
- `renderHostToString()` serializes the custom-element host together with the current view.
|
|
151
|
+
|
|
152
|
+
In practice, `renderHostToString()` is the right default for full component SSR because it emits `<my-element>...</my-element>` instead of only the view fragment.
|
|
153
|
+
|
|
154
|
+
`mode: 'hydrate'` adds hydration markers for the component view. First-connect hydration is now explicit: SSR pages should import `@ecopages/radiant/client/install-hydrator` before loading component modules, or call `installRadiantHydrator()` from `@ecopages/radiant/client/hydrator` before custom elements upgrade. Without that client hydrator gate, SSR hosts fall back to a fresh client render on first connect.
|
|
155
|
+
|
|
156
|
+
For element-owned SSR, the instance methods still work. For adapters, fragment responses, and shared server utilities, prefer the explicit helpers under `@ecopages/radiant/server/render-component`.
|
|
157
|
+
|
|
158
|
+
Server runtime setup, fragment rendering helpers, and SSR-specific import guidance now live in [src/server/README.md](src/server/README.md).
|
|
159
|
+
|
|
160
|
+
For the client lifecycle and hydration flow diagram, see [src/core/README.md](src/core/README.md).
|
|
161
|
+
|
|
162
|
+
## Event Handling
|
|
163
|
+
|
|
164
|
+
Radiant does not invent a synthetic event layer. JSX handlers and decorators work with the native browser event object directly.
|
|
165
|
+
|
|
166
|
+
| Use this | When you want | Runtime shape |
|
|
167
|
+
| -------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
168
|
+
| `on:*` in JSX | The normal event API | Auto-delegates a fixed allowlist of bubbling events and falls back to direct listeners otherwise |
|
|
169
|
+
| `on-native:*` in JSX | Exact element-level browser listener semantics | Always calls `addEventListener(...)` on that element |
|
|
170
|
+
| `@onEvent(...)` | Class-level listening from `RadiantElement` | Supports `selector`, `ref`, `window`, and `document` targets |
|
|
171
|
+
| `@event(...)` | A typed custom event emitter owned by the component | Dispatches a real `CustomEvent` from the host element |
|
|
172
|
+
|
|
173
|
+
### How JSX Event Binding Works
|
|
174
|
+
|
|
175
|
+
`on:*` is the default JSX event API. For a fixed allowlist of bubbling events, Radiant attaches one listener per event type on the render root and dispatches to matched elements. For all other events, `on:*` attaches directly on the element.
|
|
176
|
+
|
|
177
|
+
Today the delegated allowlist is:
|
|
178
|
+
|
|
179
|
+
- `beforeinput`
|
|
180
|
+
- `click`
|
|
181
|
+
- `contextmenu`
|
|
182
|
+
- `dblclick`
|
|
183
|
+
- `focusin`
|
|
184
|
+
- `focusout`
|
|
185
|
+
- `input`
|
|
186
|
+
- `keydown`
|
|
187
|
+
- `keyup`
|
|
188
|
+
- `mousedown`
|
|
189
|
+
- `mouseout`
|
|
190
|
+
- `mouseover`
|
|
191
|
+
- `mouseup`
|
|
192
|
+
- `pointerdown`
|
|
193
|
+
- `pointerout`
|
|
194
|
+
- `pointerover`
|
|
195
|
+
- `pointerup`
|
|
196
|
+
- `touchend`
|
|
197
|
+
- `touchmove`
|
|
198
|
+
- `touchstart`
|
|
199
|
+
|
|
200
|
+
Use `on-native:*` when exact attachment semantics matter, such as when an ancestor may stop propagation before the delegated root listener runs or when you want to opt out of delegation for a supported bubbling event.
|
|
201
|
+
|
|
202
|
+
Events outside the allowlist, such as `focus`, `blur`, `scroll`, `load`, `invalid`, or `dragstart`, already attach directly when authored with `on:*`.
|
|
203
|
+
|
|
204
|
+
### `@onEvent(...)`
|
|
205
|
+
|
|
206
|
+
At the class level, `@onEvent(...)` is the main decorator for incoming DOM events. It can listen to:
|
|
207
|
+
|
|
208
|
+
- descendants that match a CSS selector
|
|
209
|
+
- descendants marked with `data-ref`
|
|
210
|
+
- global `window` events
|
|
211
|
+
- global `document` events
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
/** @jsxImportSource @ecopages/jsx */
|
|
215
|
+
|
|
216
|
+
import { RadiantElement, customElement, onEvent, state } from '@ecopages/radiant';
|
|
217
|
+
|
|
218
|
+
@customElement('keyboard-panel')
|
|
219
|
+
export class KeyboardPanel extends RadiantElement<{ lastKey: string }> {
|
|
220
|
+
@state lastKey = 'none';
|
|
221
|
+
|
|
222
|
+
@onEvent({ document: true, type: 'keydown' })
|
|
223
|
+
protected onKeydown(event: KeyboardEvent): void {
|
|
224
|
+
this.lastKey = event.key;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
override render() {
|
|
228
|
+
return <p>Last key: {this.$.lastKey}</p>;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Important: `@onEvent({ selector: ... })` and `@onEvent({ ref: ... })` currently check `event.target.matches(...)` directly. That means matching is strict against the original target, not ancestor-aware. If a nested element inside a button receives the click, the nested element must match for the handler to run.
|
|
234
|
+
|
|
235
|
+
Also note that selector- and ref-based `@onEvent(...)` handlers rely on bubbling. Use `focusin` and `focusout` instead of `focus` and `blur` for that pattern.
|
|
236
|
+
|
|
237
|
+
### `@event(...)`
|
|
238
|
+
|
|
239
|
+
Outgoing component events use `@event(...)`, which gives the class a typed `EventEmitter`. Calling `.emit(detail)` dispatches a real `CustomEvent` from the host element.
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
import type { EventEmitter } from '@ecopages/radiant/tools/event-emitter';
|
|
243
|
+
import { RadiantElement, customElement, event } from '@ecopages/radiant';
|
|
244
|
+
|
|
245
|
+
type SaveDetail = {
|
|
246
|
+
id: string;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
@customElement('save-button')
|
|
250
|
+
export class SaveButton extends RadiantElement {
|
|
251
|
+
@event({ name: 'save-requested', bubbles: true, composed: true })
|
|
252
|
+
saveRequested!: EventEmitter<SaveDetail>;
|
|
253
|
+
|
|
254
|
+
requestSave(): void {
|
|
255
|
+
this.saveRequested.emit({ id: 'draft-1' });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
The mental model stays simple:
|
|
261
|
+
|
|
262
|
+
- DOM events come in through JSX handlers or `@onEvent(...)`
|
|
263
|
+
- component events go out through `@event(...)`
|
|
264
|
+
- both are ordinary browser events at runtime
|
|
265
|
+
|
|
266
|
+
## Public Entry Points
|
|
267
|
+
|
|
268
|
+
These are the documented public import paths exposed by the package.
|
|
269
|
+
|
|
270
|
+
| Path | Use for |
|
|
271
|
+
| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
272
|
+
| `@ecopages/radiant` | Main client entrypoint. Re-exports `RadiantElement`, `RadiantController`, reactive JSX bindings, common decorators, and controller-registry helpers such as `startControllers(...)` |
|
|
273
|
+
| `@ecopages/radiant/context` | Context-related exports as a grouped entrypoint |
|
|
274
|
+
| `@ecopages/radiant/context/create-context` | Creating context keys |
|
|
275
|
+
| `@ecopages/radiant/context/context-provider` | Low-level context provider class |
|
|
276
|
+
| `@ecopages/radiant/context/consume-context` | `@consumeContext(...)` decorator |
|
|
277
|
+
| `@ecopages/radiant/context/provide-context` | `@provideContext(...)` decorator |
|
|
278
|
+
| `@ecopages/radiant/context/context-selector` | `@contextSelector(...)` decorator — bind a field to context |
|
|
279
|
+
| `@ecopages/radiant/context/on-context-update` | `@onContextUpdate(...)` decorator — run a method on context change |
|
|
280
|
+
| `@ecopages/radiant/controller-registry` | Controller registration and activation helpers |
|
|
281
|
+
| `@ecopages/radiant/core/radiant-element` | Non-JSX reactive custom-element base |
|
|
282
|
+
| `@ecopages/radiant/core/radiant-controller` | DOM-attached controller base |
|
|
283
|
+
| `@ecopages/radiant/helpers/create-query` | Low-level query helper for component fields |
|
|
284
|
+
| `@ecopages/radiant/helpers/create-query-slot` | Low-level slot query helper for component fields |
|
|
285
|
+
| `@ecopages/radiant/helpers/create-event` | Low-level typed custom-event helper |
|
|
286
|
+
| `@ecopages/radiant/helpers/create-event-listener` | Low-level DOM event-listener helper |
|
|
287
|
+
| `@ecopages/radiant/client/hydrator` | Explicit client hydrator installer and status helpers for SSR pages |
|
|
288
|
+
| `@ecopages/radiant/client/install-hydrator` | Side-effect entrypoint that enables first-connect hydration before component modules load |
|
|
289
|
+
| `@ecopages/radiant/signals/host-resource` | Low-level `HostResource`, `createHostResource(...)`, and `createResource(...)` helpers |
|
|
290
|
+
| `@ecopages/radiant/server/light-dom-shim` | Minimal SSR window and host-preparation helpers |
|
|
291
|
+
| `@ecopages/radiant/server/render-component` | Canonical component SSR helpers and shared fragment metadata utilities |
|
|
292
|
+
| `@ecopages/radiant/server/render-controller` | Controller-host SSR helpers and controller-specific host option types |
|
|
293
|
+
| `@ecopages/radiant/server/project-root` | Project-root resolution helper for server adapters |
|
|
294
|
+
| `@ecopages/radiant/decorators/bound` | `@bound` |
|
|
295
|
+
| `@ecopages/radiant/decorators/custom-element` | `@customElement(...)` |
|
|
296
|
+
| `@ecopages/radiant/decorators/debounce` | `@debounce(...)` |
|
|
297
|
+
| `@ecopages/radiant/decorators/event` | `@event(...)` |
|
|
298
|
+
| `@ecopages/radiant/decorators/on-event` | `@onEvent(...)` |
|
|
299
|
+
| `@ecopages/radiant/decorators/on-updated` | `@onUpdated(...)` |
|
|
300
|
+
| `@ecopages/radiant/decorators/prop` | `@prop(...)` |
|
|
301
|
+
| `@ecopages/radiant/decorators/query` | `@query(...)` |
|
|
302
|
+
| `@ecopages/radiant/decorators/query-slot` | `@querySlot(...)` |
|
|
303
|
+
| `@ecopages/radiant/decorators/signal` | `@signal(...)` |
|
|
304
|
+
| `@ecopages/radiant/decorators/state` | `@state` |
|
|
305
|
+
| `@ecopages/radiant/tools/render-jsx-template` | Render a JSX template result into an existing host |
|
|
306
|
+
| `@ecopages/radiant/tools/escape-script-json` | Safe JSON-for-script serialization helper |
|
|
307
|
+
| `@ecopages/radiant/tools/event-emitter` | Low-level `EventEmitter` helper |
|
|
308
|
+
|
|
309
|
+
Prefer focused public subpaths when bundle size matters or when a module only needs one narrow runtime surface. The root entrypoint remains the ergonomic default for common app code, but context, registry, helper, server, signal-resource, and low-level tool imports are better taken from their explicit public subpaths.
|
|
310
|
+
|
|
311
|
+
For controller authoring, prefer a single import surface per module. If a file mixes `RadiantController`, `@controller(...)`, `@state`, `@attr(...)`, or `startControllers(...)`, default to `@ecopages/radiant` so controller registration and startup come from the same entrypoint.
|
|
312
|
+
|
|
313
|
+
Important: context APIs, controller registry helpers, helper factories, server APIs, and low-level tools have explicit public subpaths. Prefer those focused paths for library and adapter code, or when a module intentionally wants only the registry runtime without the broader client surface; the root entrypoint remains the ergonomic app entrypoint for the common client surface.
|
|
314
|
+
|
|
315
|
+
For SSR-specific guidance and examples, see [src/server/README.md](src/server/README.md).
|