@fluixi/web-components 0.0.0-stage → 1.0.0-alpha.100

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 (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +31 -2
  3. package/dist/context.cjs +162 -0
  4. package/dist/context.d.ts +65 -0
  5. package/dist/context.d.ts.map +1 -0
  6. package/dist/context.js +192 -0
  7. package/dist/context.mjs +141 -0
  8. package/dist/element.cjs +847 -0
  9. package/dist/element.d.ts +213 -0
  10. package/dist/element.d.ts.map +1 -0
  11. package/dist/element.js +681 -0
  12. package/dist/element.mjs +835 -0
  13. package/dist/events.cjs +35 -0
  14. package/dist/events.d.ts +24 -0
  15. package/dist/events.d.ts.map +1 -0
  16. package/dist/events.js +20 -0
  17. package/dist/events.mjs +14 -0
  18. package/dist/focus.cjs +156 -0
  19. package/dist/focus.d.ts +34 -0
  20. package/dist/focus.d.ts.map +1 -0
  21. package/dist/focus.js +185 -0
  22. package/dist/focus.mjs +135 -0
  23. package/dist/form.cjs +145 -0
  24. package/dist/form.d.ts +33 -0
  25. package/dist/form.d.ts.map +1 -0
  26. package/dist/form.js +113 -0
  27. package/dist/form.mjs +125 -0
  28. package/dist/index.cjs +1202 -0
  29. package/dist/index.d.ts +41 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +31 -0
  32. package/dist/index.mjs +1188 -0
  33. package/dist/props.cjs +114 -0
  34. package/dist/props.d.ts +93 -0
  35. package/dist/props.d.ts.map +1 -0
  36. package/dist/props.js +99 -0
  37. package/dist/props.mjs +93 -0
  38. package/dist/registry.cjs +77 -0
  39. package/dist/registry.d.ts +26 -0
  40. package/dist/registry.d.ts.map +1 -0
  41. package/dist/registry.js +91 -0
  42. package/dist/registry.mjs +56 -0
  43. package/dist/styles.cjs +59 -0
  44. package/dist/styles.d.ts +17 -0
  45. package/dist/styles.d.ts.map +1 -0
  46. package/dist/styles.js +53 -0
  47. package/dist/styles.mjs +38 -0
  48. package/dist/targets.cjs +42 -0
  49. package/dist/targets.d.ts +26 -0
  50. package/dist/targets.d.ts.map +1 -0
  51. package/dist/targets.js +35 -0
  52. package/dist/targets.mjs +21 -0
  53. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  54. package/dist/types.cjs +18 -0
  55. package/dist/types.d.ts +78 -0
  56. package/dist/types.d.ts.map +1 -0
  57. package/dist/types.js +1 -0
  58. package/dist/types.mjs +0 -0
  59. package/package.json +53 -3
@@ -0,0 +1,681 @@
1
+ var _a;
2
+ /**
3
+ * The custom element itself: lifecycle on Fluixi's existing ownership model.
4
+ *
5
+ * `HTMLReactiveElement` is a real class, authored the way the platform defines elements:
6
+ * `class X extends HTMLReactiveElement`, callbacks as methods, `customElements.define`.
7
+ * What it adds is the reactive part. Declared props are signals behind accessors, and the
8
+ * first connect runs `render()` once inside one `createRoot` scope; bindings update after
9
+ * that and `dispose` takes the whole subtree down.
10
+ *
11
+ * Visibility is deliberate. The platform callbacks are public because the browser calls
12
+ * them. `render`, `emit`, `targets`, `renderRoot`, `internals` and `authoredChildren` are
13
+ * protected: they are for the element's own code, not its consumers. The engine's state is
14
+ * in `#private` fields, private at runtime and not only to TypeScript.
15
+ *
16
+ * `insert` rather than `render`: `render` clears its container going in and again coming
17
+ * out (@fluixi/dom runtime.ts), which for a light-DOM element would delete the author's own
18
+ * children before the component ever saw them.
19
+ */
20
+ import { closeReactiveScope, effect, getOwner, openReactiveScope, root, runWithOwner, withReactiveScope, } from '@fluixi/reactive/signal';
21
+ import { HYDRATION_KEY, hydrateChildren, insert, isAppHydrationPending, isServer, setKeyScope, whenAppHydrated } from '@fluixi/dom';
22
+ import { createPropStates, observedFrom, } from './props.js';
23
+ import { defineCustomElement } from './registry.js';
24
+ import { createDispatch } from './events.js';
25
+ import { createTargets } from './targets.js';
26
+ import { applyStyles } from './styles.js';
27
+ import { announceOwner, consumeContext, provideContext } from './context.js';
28
+ /** Where `@fluixi/dom`'s hydration hands a server-rendered element its owner. Kept in step. */
29
+ const OWNER_KEY = Symbol.for('fluixi.owner');
30
+ /** Where @fluixi/dom keeps a server node's props, and where it looks for the element renderer. */
31
+ const SERVER_PROPS = Symbol.for('fluixi.serverProps');
32
+ const ELEMENT_RENDERER = Symbol.for('fluixi.serverElementRenderer');
33
+ /** Each element class's own server render, so the copy of this package that defined it does the work. */
34
+ const RENDER_ON_SERVER = Symbol.for('fluixi.renderOnServer');
35
+ /** Tells @fluixi/dom's page hydration that this element's children are its own, adopted already. */
36
+ const OWNS_CONTENT = Symbol.for('fluixi.ownsContent');
37
+ /** Marks content an element rendered on the server, and carries the props that have no attribute. */
38
+ const SSR_ATTRIBUTE = 'data-fx-ssr';
39
+ const SSR_PROPS_ATTRIBUTE = 'data-fx-props';
40
+ /**
41
+ * Whether a value comes back from JSON as it went in: primitives, arrays, and objects with no
42
+ * prototype but Object's. A class instance serialises without its methods and getters, so a
43
+ * Zod schema carried this way reached the client as an object with no `shape`, and the element
44
+ * read that until hydration assigned the real one. Such values wait for hydration instead.
45
+ */
46
+ function isPlainData(value, seen = new Set()) {
47
+ if (value === null)
48
+ return true;
49
+ switch (typeof value) {
50
+ case 'string':
51
+ case 'number':
52
+ case 'boolean':
53
+ return true;
54
+ case 'object':
55
+ break;
56
+ default:
57
+ return false;
58
+ }
59
+ const object = value;
60
+ if (seen.has(object))
61
+ return false;
62
+ seen.add(object);
63
+ const proto = Object.getPrototypeOf(object);
64
+ if (Array.isArray(object))
65
+ return object.every((item) => isPlainData(item, seen));
66
+ if (proto !== Object.prototype && proto !== null)
67
+ return false;
68
+ return Object.values(object).every((item) => isPlainData(item, seen));
69
+ }
70
+ /**
71
+ * `HTMLElement` in a browser. On a server there is none, and a module declaring
72
+ * `class X extends HTMLReactiveElement` is still imported by code that renders there, so
73
+ * the base is an inert stand-in: the class can be declared, nothing is ever defined.
74
+ */
75
+ const BaseElement = typeof HTMLElement === 'undefined' ? class {
76
+ } : HTMLElement;
77
+ /**
78
+ * On a server there is no `customElements` either, and an element file that registers the
79
+ * standard way (`customElements.define('x-y', X)` at the top) is still imported by code that
80
+ * renders there. A stand-in that accepts and ignores definitions keeps that import from
81
+ * throwing, as Lit's server shim does. Only installed where neither DOM global exists.
82
+ */
83
+ if (typeof HTMLElement === 'undefined' && typeof globalThis.customElements === 'undefined') {
84
+ const defined = new Map();
85
+ globalThis.customElements = {
86
+ define: (name, constructor) => void defined.set(name, constructor),
87
+ get: (name) => defined.get(name),
88
+ getName: (constructor) => [...defined].find(([, c]) => c === constructor)?.[0] ?? null,
89
+ upgrade: () => { },
90
+ // Never resolves: nothing is ever upgraded on a server.
91
+ whenDefined: () => new Promise(() => { }),
92
+ };
93
+ }
94
+ const prepared = new WeakSet();
95
+ function shadowInit(shadow) {
96
+ if (shadow === true)
97
+ return { mode: 'open' };
98
+ if (typeof shadow === 'string')
99
+ return { mode: shadow };
100
+ return { mode: 'open', ...shadow };
101
+ }
102
+ /**
103
+ * Every `static props` from the class up to this base, nearest last so a subclass can
104
+ * redeclare a prop its parent had.
105
+ */
106
+ function schemaOf(constructor) {
107
+ const chain = [];
108
+ for (let c = constructor; c && c !== HTMLReactiveElement; c = Object.getPrototypeOf(c)) {
109
+ if (Object.prototype.hasOwnProperty.call(c, 'props'))
110
+ chain.unshift(c.props ?? {});
111
+ }
112
+ return Object.assign({}, ...chain);
113
+ }
114
+ /** `el.count = 3` has to reach the signal, so the accessors go on the prototype, once per class. */
115
+ function prepare(constructor) {
116
+ if (prepared.has(constructor))
117
+ return;
118
+ prepared.add(constructor);
119
+ // A mixin that assigns `static observedAttributes = [...]` replaces this class's getter,
120
+ // and the browser then never reports the props' attributes: `step="5"` stops reaching
121
+ // `step`. Nothing fails; say which ones went missing.
122
+ const observed = constructor.observedAttributes ?? [];
123
+ const missing = observedFrom(schemaOf(constructor)).filter((a) => !observed.includes(a));
124
+ if (missing.length) {
125
+ console.warn(`[fluixi] ${constructor.name}.observedAttributes leaves out ${missing.join(', ')}, so those props no longer ` +
126
+ `follow their attributes. A class or mixin that adds its own should return [...super.observedAttributes, ...].`);
127
+ }
128
+ for (const name of Object.keys(schemaOf(constructor))) {
129
+ Object.defineProperty(constructor.prototype, name, {
130
+ configurable: true,
131
+ enumerable: true,
132
+ get() {
133
+ return stateOf(this, name).read();
134
+ },
135
+ set(value) {
136
+ writeFromOutside(this, name, value);
137
+ },
138
+ });
139
+ }
140
+ }
141
+ /** Filled by the class's static block: the one way in to its private state. */
142
+ let stateOf;
143
+ /** Filled by the class's static block: a prop written from outside, by a page or a script. */
144
+ let writeFromOutside;
145
+ export class HTMLReactiveElement extends BaseElement {
146
+ /** The declared properties, and the attributes that mirror them. Inherited and merged. */
147
+ static props;
148
+ /**
149
+ * `false` (the default) renders into the element itself. `'open'`, `'closed'`, `true` or an
150
+ * options object attach a shadow root, which is what native `<slot>` needs. Light DOM is the default: a
151
+ * component that inherits the page's styles is usually what an app wants, and it is the
152
+ * mode that survives a server render without declarative shadow DOM.
153
+ */
154
+ static shadow;
155
+ /**
156
+ * Adopted by the shadow root, one parsed sheet shared by every instance. CSS text, a
157
+ * `CSSStyleSheet`, or a list: `[super.styles, '…']` keeps the parent's. A class meant to be
158
+ * extended declares it as `static styles: ElementStyles = …`, since a bare string narrows
159
+ * the type and a subclass's list no longer fits. Ignored in light DOM, which has no scope.
160
+ */
161
+ static styles;
162
+ /**
163
+ * The attributes of every declared prop. A mixin in between that observes its own
164
+ * extends this with `super.observedAttributes`; a subclass need not touch it.
165
+ */
166
+ static get observedAttributes() {
167
+ prepare(this);
168
+ return observedFrom(schemaOf(this));
169
+ }
170
+ static {
171
+ // Not a member in the type: it is a handshake with the runtime, not API.
172
+ Object.defineProperty(_a.prototype, OWNER_KEY, {
173
+ configurable: true,
174
+ get() {
175
+ return this.#creator;
176
+ },
177
+ set(owner) {
178
+ this.#adoptOwner(owner);
179
+ },
180
+ });
181
+ // On a server, @fluixi/dom hands each attached custom element node to this renderer. It
182
+ // defers to the element's class: after a dev reload, or with two bundles of this package,
183
+ // the renderer registered first belongs to another copy whose classes are not this one's.
184
+ if (typeof HTMLElement === 'undefined') {
185
+ globalThis[ELEMENT_RENDERER] ??= (node) => {
186
+ const registry = globalThis.customElements;
187
+ const constructor = registry?.get(node.localName);
188
+ constructor?.[RENDER_ON_SERVER]?.(node);
189
+ };
190
+ }
191
+ stateOf = (element, name) => {
192
+ const state = element.#states()[name];
193
+ if (!state)
194
+ throw new Error(`[fluixi] ${name} is not a declared prop of <${element.localName}>`);
195
+ return state;
196
+ };
197
+ writeFromOutside = (element, name, value) => element.#writeFromOutside(name, value);
198
+ }
199
+ /**
200
+ * Props written while the app hydrates around an element already built, applied once it is
201
+ * done. The page hydrates inside a batch whose effects run before hydration ends: an update
202
+ * run there builds the element's new content while server nodes are still being claimed,
203
+ * and its templates claimed the page's nodes as their own (the page's frame moved into the
204
+ * element). Until then the element shows what it adopted from the server.
205
+ */
206
+ #deferred = null;
207
+ #writeFromOutside(name, value, apply = () => stateOf(this, name).write(value)) {
208
+ if (!this.#built || !isAppHydrationPending()) {
209
+ apply();
210
+ return;
211
+ }
212
+ if (!this.#deferred) {
213
+ this.#deferred = new Map();
214
+ whenAppHydrated(() => {
215
+ const pending = this.#deferred;
216
+ this.#deferred = null;
217
+ for (const write of pending?.values() ?? [])
218
+ write();
219
+ });
220
+ }
221
+ this.#deferred.set(name, apply);
222
+ }
223
+ #statesCache = null;
224
+ /** Prop values kept across a real removal, for the signals the next scope re-creates. */
225
+ #carried = null;
226
+ #dispose = null;
227
+ #root = this;
228
+ #authored = [];
229
+ #built = false;
230
+ /** Server-rendered and waiting for the app to hand over an owner; cancels the wait. */
231
+ #waiting = null;
232
+ /** The wait ran once; whatever it found, the next build goes ahead. */
233
+ #waited = false;
234
+ /**
235
+ * A template or hydration handed the owner over. An upgrade reads whatever owner happens to be
236
+ * current (a module evaluating inside a route preload), which is not this element's host.
237
+ */
238
+ #handed = false;
239
+ /** The first build adopts content the server rendered. */
240
+ #hydrating = false;
241
+ /**
242
+ * Disposal is deferred because `disconnectedCallback` cannot tell a removal from a move.
243
+ * Appending an already-parented node fires disconnect then connect in the same task, and
244
+ * tearing the scope down in between would rebuild the subtree on every reorder, which a
245
+ * keyed list does constantly.
246
+ */
247
+ #pendingDispose = null;
248
+ /** Suppresses the attribute callback a reflect triggers, so it does not loop. */
249
+ #reflecting = new Set();
250
+ #internals = null;
251
+ /**
252
+ * What the devtools graph groups this element's signals under. Opened when the element
253
+ * first needs one, closed when it is really removed.
254
+ */
255
+ #scope = null;
256
+ #targets = null;
257
+ #dispatch = null;
258
+ /** The owner the element was created under, for context: a Fluixi template, or none. */
259
+ #creator = getOwner();
260
+ /** The owner `render()` runs under, while built. Its parent is the creator. */
261
+ #rootOwner = null;
262
+ constructor() {
263
+ super();
264
+ prepare(new.target);
265
+ // Hydration reached this element before its definition did, and left the owner behind.
266
+ const handed = Object.getOwnPropertyDescriptor(this, OWNER_KEY);
267
+ if (handed) {
268
+ delete this[OWNER_KEY];
269
+ this.#creator = handed.value ?? this.#creator;
270
+ this.#handed = handed.value != null;
271
+ }
272
+ // A property assigned before upgrade sits as an own property and shadows the prototype
273
+ // accessor. Take it off and replay it through the signal.
274
+ this.#adoptOwnProps();
275
+ }
276
+ connectedCallback() {
277
+ // What sits above first: a mixin expects to run before the class that composed it.
278
+ Object.getPrototypeOf(_a.prototype).connectedCallback?.call(this);
279
+ if (this.#pendingDispose !== null) {
280
+ clearTimeout(this.#pendingDispose);
281
+ this.#pendingDispose = null;
282
+ return; // A move. The scope is alive and the DOM is still ours.
283
+ }
284
+ // A class field named like a prop lands after the constructor and hides the accessor.
285
+ this.#adoptOwnProps(true);
286
+ if (!this.#built && !this.#waiting)
287
+ this.#build();
288
+ }
289
+ disconnectedCallback() {
290
+ Object.getPrototypeOf(_a.prototype).disconnectedCallback?.call(this);
291
+ if (!this.#built || this.#pendingDispose !== null)
292
+ return;
293
+ this.#pendingDispose = setTimeout(() => {
294
+ this.#pendingDispose = null;
295
+ this.#teardown();
296
+ }, 0);
297
+ }
298
+ /** Another document, the same scope: nothing reactive depends on which document holds the node. */
299
+ adoptedCallback() {
300
+ Object.getPrototypeOf(_a.prototype).adoptedCallback?.call(this);
301
+ }
302
+ attributeChangedCallback(name, previous, value) {
303
+ Object.getPrototypeOf(_a.prototype).attributeChangedCallback?.call(this, name, previous, value);
304
+ if (this.#reflecting.has(name))
305
+ return;
306
+ for (const [prop, state] of Object.entries(this.#states())) {
307
+ if (state.attribute !== name)
308
+ continue;
309
+ this.#writeFromOutside(prop, value, () => state.write(state.declaration.fromAttribute(value)));
310
+ return;
311
+ }
312
+ }
313
+ /**
314
+ * What the element shows. Runs once, on the first connect, inside the element's reactive
315
+ * scope; bindings keep it current after that. It is never re-run, so read a prop inside a
316
+ * binding (`() => this.step`), not in the body, to follow it.
317
+ */
318
+ render() {
319
+ return undefined;
320
+ }
321
+ /**
322
+ * Give `context` to the elements inside this one, and to `useContext` in the components it
323
+ * renders. A value or an accessor; subscribed consumers follow the accessor. Call in `render()`.
324
+ */
325
+ provide(context, value) {
326
+ provideContext(this, context, value);
327
+ }
328
+ /**
329
+ * The nearest value of `context` above this element, as an accessor: an element providing
330
+ * it, else the Fluixi app the element was rendered in, else the default. Call in `render()`.
331
+ */
332
+ consume(context, options) {
333
+ return consumeContext(this, context, options);
334
+ }
335
+ /**
336
+ * Dispatch a `CustomEvent`, bubbling and composed by default. False when cancelled.
337
+ *
338
+ * The name goes out lowercased: a JSX handler listens for the name after `on`, lowercased,
339
+ * so `emit('signOut')` reaches `onSignOut`, and a listener added by hand uses `signout`.
340
+ */
341
+ emit(type, detail, init) {
342
+ this.#dispatch ??= createDispatch(this);
343
+ return this.#dispatch(type.toLowerCase(), detail, init);
344
+ }
345
+ /** Elements inside this one, by `data-target`, scoped to its render root. */
346
+ get targets() {
347
+ return (this.#targets ??= createTargets(() => this.#root));
348
+ }
349
+ /** Where `render()` output goes: the shadow root, or the element itself in light DOM. */
350
+ get renderRoot() {
351
+ return this.#root;
352
+ }
353
+ /**
354
+ * The children the author wrote between the tags, in light DOM. Empty under shadow DOM,
355
+ * where the browser projects them through `<slot>` and they were never the element's to place.
356
+ */
357
+ get authoredChildren() {
358
+ return this.#authored;
359
+ }
360
+ /**
361
+ * The element's `ElementInternals`, attached on first use: ARIA defaults, custom states,
362
+ * and with `static formAssociated = true`, taking part in a `<form>`.
363
+ */
364
+ get internals() {
365
+ return (this.#internals ??= this.attachInternals());
366
+ }
367
+ #states() {
368
+ if (this.#statesCache)
369
+ return this.#statesCache;
370
+ const states = withReactiveScope(this.#reactiveScope(), () => createPropStates(schemaOf(this.constructor)));
371
+ if (this.#carried) {
372
+ for (const [name, value] of Object.entries(this.#carried))
373
+ states[name]?.write(value);
374
+ this.#carried = null;
375
+ }
376
+ return (this.#statesCache = states);
377
+ }
378
+ #reactiveScope() {
379
+ return (this.#scope ??= openReactiveScope({
380
+ kind: 'element',
381
+ name: `<${this.localName}>`,
382
+ target: this,
383
+ // The class, since the line that opened the scope is this package's, not the app's.
384
+ detail: this.constructor.name,
385
+ }));
386
+ }
387
+ #adoptOwnProps(warn = false) {
388
+ for (const name of Object.keys(schemaOf(this.constructor))) {
389
+ if (!Object.prototype.hasOwnProperty.call(this, name))
390
+ continue;
391
+ const value = this[name];
392
+ delete this[name];
393
+ if (warn) {
394
+ console.warn(`[fluixi] <${this.localName}> has a class field named "${name}", which hides the declared prop. ` +
395
+ `Declare it only in static props, and set a default there.`);
396
+ }
397
+ this.#states()[name].write(value);
398
+ }
399
+ }
400
+ /**
401
+ * Render a light DOM element's content into its server node, under the page's owner so its
402
+ * context and resources are the page's. Props come from what @fluixi/dom recorded on the
403
+ * node; the ones with no attribute form ride along as JSON for the client. Shadow DOM
404
+ * elements are left to the client until declarative shadow roots are emitted.
405
+ */
406
+ static [RENDER_ON_SERVER](node) {
407
+ _a.#renderOnServer(this, node);
408
+ }
409
+ static #renderOnServer(constructor, node) {
410
+ if (constructor.shadow !== undefined && constructor.shadow !== false)
411
+ return;
412
+ const element = new constructor();
413
+ const recorded = node[SERVER_PROPS] ?? {};
414
+ const carried = {};
415
+ for (const [name, state] of Object.entries(element.#states())) {
416
+ if (name in recorded) {
417
+ const value = recorded[name];
418
+ state.write(value);
419
+ if (value !== null && typeof value === 'object' && isPlainData(value))
420
+ carried[name] = value;
421
+ }
422
+ else if (state.attribute !== false) {
423
+ const attribute = node.getAttribute(state.attribute);
424
+ if (attribute !== null)
425
+ state.write(state.declaration.fromAttribute(attribute));
426
+ }
427
+ }
428
+ element.#authored = [...node.childNodes];
429
+ for (const child of element.#authored)
430
+ node.removeChild(child);
431
+ element.#root = node;
432
+ element.#built = true;
433
+ // Its own key scope, named after its own key: the client renders this content in the
434
+ // element's root, not in the page's, and must number it the same way.
435
+ const scope = `${node.getAttribute(HYDRATION_KEY) ?? ''}~`;
436
+ // Under the scope that created the node (recorded by @fluixi/dom), so the app's providers
437
+ // reach `useContext` here as they do in the browser.
438
+ const createdIn = node[OWNER_KEY] ?? null;
439
+ runWithOwner(createdIn, () => root(() => {
440
+ setKeyScope(scope);
441
+ insert(node, element.render());
442
+ }));
443
+ node.setAttribute(SSR_ATTRIBUTE, '');
444
+ if (Object.keys(carried).length) {
445
+ try {
446
+ node.setAttribute(SSR_PROPS_ATTRIBUTE, JSON.stringify(carried));
447
+ }
448
+ catch {
449
+ // A cycle: the client waits for hydration to assign it.
450
+ }
451
+ }
452
+ }
453
+ #build() {
454
+ if (this.#shouldWaitForHost())
455
+ return this.#waitForHost();
456
+ this.#built = true;
457
+ const constructor = this.constructor;
458
+ const shadow = constructor.shadow;
459
+ if (shadow !== undefined && shadow !== false) {
460
+ // A declarative shadow root may already be attached from a server render.
461
+ this.#root = this.shadowRoot ?? this.attachShadow(shadowInit(shadow));
462
+ applyStyles(this.#root, constructor, constructor.styles);
463
+ }
464
+ else if (this.hasAttribute(SSR_ATTRIBUTE)) {
465
+ // Rendered on the server: its children are render()'s output, adopted below, and the
466
+ // props with no attribute form come back from the JSON the server wrote.
467
+ this.#hydrating = true;
468
+ const json = this.getAttribute(SSR_PROPS_ATTRIBUTE);
469
+ if (json) {
470
+ try {
471
+ const states = this.#states();
472
+ for (const [name, value] of Object.entries(JSON.parse(json)))
473
+ states[name]?.write(value);
474
+ }
475
+ catch {
476
+ // Malformed: hydration assigns them as properties.
477
+ }
478
+ }
479
+ this.removeAttribute(SSR_ATTRIBUTE);
480
+ this.removeAttribute(SSR_PROPS_ATTRIBUTE);
481
+ this[OWNS_CONTENT] = true;
482
+ this.#root = this;
483
+ }
484
+ else {
485
+ // Light DOM: the author's children are the content. Take them out before anything
486
+ // renders, so they survive and the component can place them.
487
+ this.#authored = Array.from(this.childNodes);
488
+ for (const node of this.#authored)
489
+ node.remove();
490
+ this.#root = this;
491
+ }
492
+ // Detached: the element lives as long as it is connected, not as long as whatever created
493
+ // it. Linked to that creator for context only, so an app's providers reach `render()`.
494
+ const creator = this.#creator ?? getOwner();
495
+ runWithOwner(null, () => withReactiveScope(this.#reactiveScope(), () => root((dispose) => {
496
+ const own = getOwner();
497
+ this.#rootOwner = own;
498
+ if (own && creator)
499
+ own.parent = creator;
500
+ this.#dispose = dispose;
501
+ // Reflects live in the same scope as the content, so they stop when it does.
502
+ this.#startReflecting();
503
+ if (this.#hydrating) {
504
+ // The scope the server rendered this content in (see renderOnServer).
505
+ setKeyScope(`${this.getAttribute(HYDRATION_KEY) ?? ''}~`);
506
+ hydrateChildren(this, () => this.render());
507
+ }
508
+ else
509
+ insert(this.#root, this.render());
510
+ this.#hydrating = false;
511
+ })));
512
+ // In light DOM the element decides where the children written between its tags go. One
513
+ // that places none of them hides them, which is silent unless said.
514
+ const shown = (node) => node.nodeType === 1 || (node.nodeType === 3 && node.textContent?.trim());
515
+ if (this.#root === this && this.#authored.some((node) => shown(node) && !node.isConnected)) {
516
+ console.warn(`[fluixi] <${this.localName}> has children between its tags that its render() does not place, so they ` +
517
+ `are not shown. Place this.authoredChildren in render(), or use shadow DOM with a <slot>.`);
518
+ }
519
+ }
520
+ #startReflecting() {
521
+ for (const state of Object.values(this.#states())) {
522
+ const attribute = state.attribute;
523
+ if (attribute === false || !state.declaration.reflect)
524
+ continue;
525
+ effect(() => {
526
+ const next = state.declaration.toAttribute(state.read());
527
+ this.#reflecting.add(attribute);
528
+ try {
529
+ if (next === null)
530
+ this.removeAttribute(attribute);
531
+ else
532
+ this.setAttribute(attribute, next);
533
+ }
534
+ finally {
535
+ this.#reflecting.delete(attribute);
536
+ }
537
+ }, undefined, { name: `reflect ${attribute}` });
538
+ }
539
+ }
540
+ /**
541
+ * Hydration found this element after it had already rendered, as a server-rendered one does:
542
+ * link the render to the app now, so what it provides reaches the element. Consumers that
543
+ * found nothing are told to look again.
544
+ */
545
+ /**
546
+ * Rendered by the server inside an app, and upgraded before that app hydrated: nothing
547
+ * above it yet, so a context read in its first render would find nothing. Light DOM only;
548
+ * a shadow root is built by the client.
549
+ */
550
+ #shouldWaitForHost() {
551
+ const shadow = this.constructor.shadow;
552
+ return !this.#waited && !this.#handed && (shadow === undefined || shadow === false) && this.hasAttribute(SSR_ATTRIBUTE);
553
+ }
554
+ /**
555
+ * Wait a microtask, so an entry module that imported this element has called `startClient`.
556
+ * Then build when the app hands over an owner, or when its hydration ends without reaching
557
+ * this element. No app hydrating (a static page, another framework): build now.
558
+ */
559
+ #waitForHost() {
560
+ let cancelled = false;
561
+ let stop = () => { };
562
+ this.#waiting = () => {
563
+ cancelled = true;
564
+ stop();
565
+ };
566
+ const resume = () => {
567
+ if (cancelled)
568
+ return;
569
+ this.#waiting = null;
570
+ this.#waited = true;
571
+ if (!this.#built && this.isConnected)
572
+ this.#build();
573
+ };
574
+ queueMicrotask(() => {
575
+ if (cancelled)
576
+ return;
577
+ if (isAppHydrationPending())
578
+ stop = whenAppHydrated(resume);
579
+ else
580
+ resume();
581
+ });
582
+ }
583
+ #adoptOwner(owner) {
584
+ if (!owner || this.#handed)
585
+ return;
586
+ this.#handed = true;
587
+ this.#creator = owner;
588
+ if (this.#waiting) {
589
+ this.#waiting();
590
+ this.#waiting = null;
591
+ if (this.isConnected)
592
+ this.#build();
593
+ return;
594
+ }
595
+ if (!this.#rootOwner)
596
+ return;
597
+ this.#rootOwner.parent = owner;
598
+ announceOwner(this);
599
+ }
600
+ #teardown() {
601
+ this.#dispose?.();
602
+ this.#dispose = null;
603
+ this.#rootOwner = null;
604
+ this.#built = false;
605
+ if (this.#scope)
606
+ closeReactiveScope(this.#scope);
607
+ this.#scope = null;
608
+ // The props outlive the scope: their values move to new signals made inside the next one,
609
+ // so an element added back shows its props under the scope it reports then.
610
+ if (this.#statesCache) {
611
+ this.#carried = Object.fromEntries(Object.entries(this.#statesCache).map(([name, state]) => [name, state.read()]));
612
+ this.#statesCache = null;
613
+ }
614
+ // The rendered content goes with the scope. Light children were taken out at build time
615
+ // and are put back, so the element reads as it was authored.
616
+ if (this.#root === this) {
617
+ this.textContent = '';
618
+ for (const node of this.#authored)
619
+ this.appendChild(node);
620
+ }
621
+ }
622
+ }
623
+ _a = HTMLReactiveElement;
624
+ /**
625
+ * `HTMLReactiveElement` with the declared props typed on the instance:
626
+ * `class Counter extends typedElement(props)` gives `this.step` its type. Static props alone
627
+ * work at runtime; this is only the type.
628
+ */
629
+ export function typedElement(props) {
630
+ class Typed extends HTMLReactiveElement {
631
+ static props = props;
632
+ }
633
+ return Typed;
634
+ }
635
+ export function createElementClass(setup, options = {}) {
636
+ let Base = HTMLReactiveElement;
637
+ // Either shape takes what the previous one returned: the runtime class is the same.
638
+ for (const mixin of options.mixins ?? [])
639
+ Base = mixin(Base);
640
+ class SetupElement extends Base {
641
+ static props = options.props ?? {};
642
+ static shadow = options.shadow;
643
+ static styles = options.styles;
644
+ render() {
645
+ const accessors = {};
646
+ for (const name of Object.keys(schemaOf(this.constructor))) {
647
+ accessors[name] = () => this[name];
648
+ }
649
+ return setup({
650
+ el: this,
651
+ props: accessors,
652
+ set: (name, value) => {
653
+ this[name] = value;
654
+ },
655
+ dispatch: (type, detail, init) => this.emit(type, detail, init),
656
+ targets: this.targets,
657
+ children: () => [...this.authoredChildren],
658
+ provide: (context, value) => this.provide(context, value),
659
+ consume: (context, options) => this.consume(context, options),
660
+ });
661
+ }
662
+ }
663
+ return SetupElement;
664
+ }
665
+ /**
666
+ * Define a Fluixi component as a custom element.
667
+ *
668
+ * Returns the constructor, so a caller can subclass it or hand it to a lazy loader. On a
669
+ * server it builds the class and registers nothing, which keeps the module importable from
670
+ * code that also runs during a server render.
671
+ */
672
+ export function defineElement(name, setup, options = {}) {
673
+ const constructor = createElementClass(setup, options);
674
+ // `fx-disclosure` → `FxDisclosure`, rather than the generated `SetupElement`: what a stack
675
+ // trace and the devtools graph show for this element.
676
+ const className = name.replace(/(^|-)([a-z0-9])/g, (_, _dash, ch) => ch.toUpperCase());
677
+ Object.defineProperty(constructor, 'name', { value: className, configurable: true });
678
+ if (isServer())
679
+ return constructor;
680
+ return defineCustomElement(name, constructor, options.define);
681
+ }