@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ibrahima Touré
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,32 @@
1
- # Temporary Holding Version
1
+ # @fluixi/web-components
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Standard custom elements with Fluixi's reactivity inside. They work in any page and any
4
+ framework, and in a Fluixi app they render on the server and hydrate like the rest of the page.
5
+
6
+ ```ts
7
+ import { defineCustomElement, prop, typedElement } from '@fluixi/web-components';
8
+ import { html } from '@fluixi/dom';
9
+ import { signal } from '@fluixi/reactive/signal';
10
+
11
+ class FxCounter extends typedElement({ step: prop.number(1) }) {
12
+ #count = signal(0);
13
+
14
+ protected override render() {
15
+ return html`<button @click=${() => this.#count.set(this.#count() + this.step)}>${() => this.#count()}</button>`;
16
+ }
17
+ }
18
+
19
+ defineCustomElement('fx-counter', FxCounter);
20
+ ```
21
+
22
+ - **Props**: `prop.string`, `number`, `boolean`, `object`, `custom` and `any`; a property and
23
+ its attribute share one signal, with `reflect` and `attribute` options.
24
+ - **Events**: `this.emit(type, detail)`, typed for consumers through `ElementAttributes`.
25
+ - **Styles**: light DOM by default; `static shadow` and shared constructed `static styles`.
26
+ - **Context**: `provide` and `consume` between elements, interoperable with `@lit/context`, and
27
+ the app's own providers.
28
+ - **Forms and focus**: `FormAssociated`, `rovingFocus`, `trapFocus`, `tabbables`.
29
+ - **Setup form**: `defineElement(name, setup, { props })` without a class.
30
+ - **Loading**: `defineWhenUsed` registers an element only once its tag appears.
31
+
32
+ Documentation: [Web components](https://docs.fluixi.com/v1/en/web-components).
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/context.ts
21
+ var context_exports = {};
22
+ __export(context_exports, {
23
+ ContextProviderEvent: () => ContextProviderEvent,
24
+ ContextRequestEvent: () => ContextRequestEvent,
25
+ announceOwner: () => announceOwner,
26
+ consumeContext: () => consumeContext,
27
+ provideContext: () => provideContext
28
+ });
29
+ module.exports = __toCommonJS(context_exports);
30
+ var import_signal = require("@fluixi/reactive/signal");
31
+ var ContextRequestEvent = class extends Event {
32
+ constructor(context, contextTarget, callback, subscribe) {
33
+ super("context-request", { bubbles: true, composed: true });
34
+ this.context = context;
35
+ this.contextTarget = contextTarget;
36
+ this.callback = callback;
37
+ this.subscribe = subscribe;
38
+ }
39
+ };
40
+ var ContextProviderEvent = class extends Event {
41
+ constructor(context, contextTarget) {
42
+ super("context-provider", { bubbles: true, composed: true });
43
+ this.context = context;
44
+ this.contextTarget = contextTarget;
45
+ }
46
+ };
47
+ var ContextOwnerEvent = class extends Event {
48
+ constructor(element) {
49
+ super("fluixi-context-owner");
50
+ this.element = element;
51
+ }
52
+ };
53
+ function announceOwner(element) {
54
+ element.ownerDocument.dispatchEvent(new ContextOwnerEvent(element));
55
+ }
56
+ function matches(key, context) {
57
+ return key === context || typeof key === "object" && key !== null && key.id === context.id;
58
+ }
59
+ function describe(context) {
60
+ return context.id.description ?? "context";
61
+ }
62
+ function provideContext(host, context, value) {
63
+ const read = typeof value === "function" ? value : () => value;
64
+ const subscribers = /* @__PURE__ */ new Map();
65
+ const onRequest = (event) => {
66
+ const request = event;
67
+ if (!matches(request.context, context) || request.contextTarget === host) return;
68
+ event.stopImmediatePropagation();
69
+ if (!request.subscribe) {
70
+ request.callback((0, import_signal.untrack)(read));
71
+ return;
72
+ }
73
+ const unsubscribe = () => void subscribers.delete(request.callback);
74
+ subscribers.set(request.callback, unsubscribe);
75
+ request.callback((0, import_signal.untrack)(read), unsubscribe);
76
+ };
77
+ const dom = typeof host.addEventListener === "function";
78
+ if (dom) host.addEventListener("context-request", onRequest);
79
+ const owner = (0, import_signal.getOwner)();
80
+ let first = true;
81
+ const stopTracking = (0, import_signal.effect)(
82
+ () => {
83
+ const next = read();
84
+ if (owner) (owner.contexts ??= /* @__PURE__ */ new Map()).set(context.id, next);
85
+ if (first) {
86
+ first = false;
87
+ return;
88
+ }
89
+ for (const [callback, unsubscribe] of subscribers) callback(next, unsubscribe);
90
+ },
91
+ void 0,
92
+ { name: `provide ${describe(context)}` }
93
+ );
94
+ if (dom) {
95
+ queueMicrotask(() => {
96
+ if (!stopped) host.dispatchEvent(new ContextProviderEvent(context, host));
97
+ });
98
+ }
99
+ let stopped = false;
100
+ const stop = () => {
101
+ if (stopped) return;
102
+ stopped = true;
103
+ if (dom) host.removeEventListener("context-request", onRequest);
104
+ stopTracking();
105
+ subscribers.clear();
106
+ };
107
+ (0, import_signal.onCleanup)(stop);
108
+ return stop;
109
+ }
110
+ function consumeContext(host, context, options = {}) {
111
+ const subscribe = options.subscribe ?? true;
112
+ const owner = (0, import_signal.getOwner)();
113
+ const fromTree = () => (0, import_signal.runWithOwner)(owner, () => (0, import_signal.useContext)(context));
114
+ const value = (0, import_signal.signal)(fromTree(), { name: describe(context) });
115
+ if (typeof host.dispatchEvent !== "function") return () => value();
116
+ let answered = false;
117
+ let unsubscribe;
118
+ const ask = () => {
119
+ host.dispatchEvent(
120
+ new ContextRequestEvent(
121
+ context,
122
+ host,
123
+ (next, stop) => {
124
+ answered = true;
125
+ if (stop) unsubscribe = stop;
126
+ value.set(() => next);
127
+ },
128
+ subscribe
129
+ )
130
+ );
131
+ };
132
+ const doc = host.ownerDocument;
133
+ const onProvider = (event) => {
134
+ const announced = event;
135
+ if (answered || !matches(announced.context, context)) return;
136
+ if (containsComposed(announced.contextTarget, host)) ask();
137
+ };
138
+ const onOwner = (event) => {
139
+ if (answered || !containsComposed(event.element, host)) return;
140
+ const next = fromTree();
141
+ value.set(() => next);
142
+ };
143
+ ask();
144
+ if (!answered) {
145
+ doc.addEventListener("context-provider", onProvider);
146
+ doc.addEventListener("fluixi-context-owner", onOwner);
147
+ }
148
+ (0, import_signal.onCleanup)(() => {
149
+ unsubscribe?.();
150
+ doc.removeEventListener("context-provider", onProvider);
151
+ doc.removeEventListener("fluixi-context-owner", onOwner);
152
+ });
153
+ return () => value();
154
+ }
155
+ function containsComposed(container, node) {
156
+ let at = node;
157
+ while (at) {
158
+ if (at === container) return true;
159
+ at = at.parentNode ?? (at instanceof ShadowRoot ? at.host : null);
160
+ }
161
+ return false;
162
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Context between elements, over the Web Components Community Group protocol.
3
+ *
4
+ * A consumer dispatches `context-request` from itself; it bubbles, crosses shadow roots, and
5
+ * the nearest provider of that context answers and stops it. With `subscribe` the provider
6
+ * calls back again on every change. A provider that turns up late (its tag upgraded after the
7
+ * consumer asked) announces itself with `context-provider`, and consumers still waiting ask
8
+ * again. Lit's `@lit/context` speaks the same protocol, so the two interoperate when they share
9
+ * the context object.
10
+ *
11
+ * The key is a Fluixi context (`createContext` from `@fluixi/core` or `@fluixi/reactive`), which
12
+ * also joins the two trees: an element that finds no provider above it in the DOM reads the
13
+ * owner tree it was created in, and what an element provides is visible to `useContext` in the
14
+ * components it renders.
15
+ */
16
+ import { type Accessor } from '@fluixi/reactive/signal';
17
+ /** A context key. `createContext` in `@fluixi/core` and `@fluixi/reactive` both return one. */
18
+ export interface ElementContext<T> {
19
+ readonly id: symbol;
20
+ readonly defaultValue?: T;
21
+ }
22
+ type ContextCallback<T> = (value: T, unsubscribe?: () => void) => void;
23
+ /** Asks for a context. Field names and order follow the protocol, and so Lit. */
24
+ export declare class ContextRequestEvent<T = unknown> extends Event {
25
+ readonly context: unknown;
26
+ readonly contextTarget: Element;
27
+ readonly callback: ContextCallback<T>;
28
+ readonly subscribe?: boolean | undefined;
29
+ constructor(context: unknown, contextTarget: Element, callback: ContextCallback<T>, subscribe?: boolean | undefined);
30
+ }
31
+ /** A provider that has just become available, so a consumer still waiting can ask again. */
32
+ export declare class ContextProviderEvent extends Event {
33
+ readonly context: unknown;
34
+ readonly contextTarget: Element;
35
+ constructor(context: unknown, contextTarget: Element);
36
+ }
37
+ /** Tell consumers under `element` that the owner tree above them changed. */
38
+ export declare function announceOwner(element: Element): void;
39
+ declare global {
40
+ interface HTMLElementEventMap {
41
+ 'context-request': ContextRequestEvent;
42
+ 'context-provider': ContextProviderEvent;
43
+ }
44
+ }
45
+ /**
46
+ * Answer `context-request` for `context` from inside `host`. `value` is a value or an accessor;
47
+ * with an accessor, subscribed consumers are called again when it changes. Stops when the
48
+ * reactive scope it was called in ends: call it in `render()`.
49
+ */
50
+ export declare function provideContext<T>(host: HTMLElement, context: ElementContext<T>, value: T | Accessor<T>): () => void;
51
+ export interface ConsumeOptions {
52
+ /** Keep receiving the provider's changes. Defaults to true. */
53
+ subscribe?: boolean;
54
+ }
55
+ /**
56
+ * The nearest value of `context` above `host`, as an accessor.
57
+ *
58
+ * Asked of the DOM first. With no provider there, it is the value in the owner tree the element
59
+ * was created in, so `<Ctx.Provider>` in a Fluixi app reaches an element rendered inside it, and
60
+ * otherwise the context's default. A provider that appears later takes over. Stops when the
61
+ * reactive scope it was called in ends: call it in `render()`.
62
+ */
63
+ export declare function consumeContext<T>(host: HTMLElement, context: ElementContext<T>, options?: ConsumeOptions): Accessor<T>;
64
+ export {};
65
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAA0E,KAAK,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEhI,+FAA+F;AAC/F,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;CAC3B;AAED,KAAK,eAAe,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,WAAW,CAAC,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;AAEvE,iFAAiF;AACjF,qBAAa,mBAAmB,CAAC,CAAC,GAAG,OAAO,CAAE,SAAQ,KAAK;IAEvD,QAAQ,CAAC,OAAO,EAAE,OAAO;IACzB,QAAQ,CAAC,aAAa,EAAE,OAAO;IAC/B,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC,CAAC;IACrC,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO;gBAHnB,OAAO,EAAE,OAAO,EAChB,aAAa,EAAE,OAAO,EACtB,QAAQ,EAAE,eAAe,CAAC,CAAC,CAAC,EAC5B,SAAS,CAAC,EAAE,OAAO,YAAA;CAI/B;AAED,4FAA4F;AAC5F,qBAAa,oBAAqB,SAAQ,KAAK;IAE3C,QAAQ,CAAC,OAAO,EAAE,OAAO;IACzB,QAAQ,CAAC,aAAa,EAAE,OAAO;gBADtB,OAAO,EAAE,OAAO,EAChB,aAAa,EAAE,OAAO;CAIlC;AAaD,6EAA6E;AAC7E,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAEpD;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,mBAAmB;QAC3B,iBAAiB,EAAE,mBAAmB,CAAC;QACvC,kBAAkB,EAAE,oBAAoB,CAAC;KAC1C;CACF;AAcD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,CAyDnH;AAED,MAAM,WAAW,cAAc;IAC7B,+DAA+D;IAC/D,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,EAAE,OAAO,GAAE,cAAmB,GAAG,QAAQ,CAAC,CAAC,CAAC,CAoD1H"}
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Context between elements, over the Web Components Community Group protocol.
3
+ *
4
+ * A consumer dispatches `context-request` from itself; it bubbles, crosses shadow roots, and
5
+ * the nearest provider of that context answers and stops it. With `subscribe` the provider
6
+ * calls back again on every change. A provider that turns up late (its tag upgraded after the
7
+ * consumer asked) announces itself with `context-provider`, and consumers still waiting ask
8
+ * again. Lit's `@lit/context` speaks the same protocol, so the two interoperate when they share
9
+ * the context object.
10
+ *
11
+ * The key is a Fluixi context (`createContext` from `@fluixi/core` or `@fluixi/reactive`), which
12
+ * also joins the two trees: an element that finds no provider above it in the DOM reads the
13
+ * owner tree it was created in, and what an element provides is visible to `useContext` in the
14
+ * components it renders.
15
+ */
16
+ import { effect, getOwner, onCleanup, runWithOwner, signal, untrack, useContext } from '@fluixi/reactive/signal';
17
+ /** Asks for a context. Field names and order follow the protocol, and so Lit. */
18
+ export class ContextRequestEvent extends Event {
19
+ context;
20
+ contextTarget;
21
+ callback;
22
+ subscribe;
23
+ constructor(context, contextTarget, callback, subscribe) {
24
+ super('context-request', { bubbles: true, composed: true });
25
+ this.context = context;
26
+ this.contextTarget = contextTarget;
27
+ this.callback = callback;
28
+ this.subscribe = subscribe;
29
+ }
30
+ }
31
+ /** A provider that has just become available, so a consumer still waiting can ask again. */
32
+ export class ContextProviderEvent extends Event {
33
+ context;
34
+ contextTarget;
35
+ constructor(context, contextTarget) {
36
+ super('context-provider', { bubbles: true, composed: true });
37
+ this.context = context;
38
+ this.contextTarget = contextTarget;
39
+ }
40
+ }
41
+ /**
42
+ * An element has just been linked to the app it was rendered in: hydration reached it after it
43
+ * had rendered. Consumers inside it that found no provider read the owner tree again. Fluixi's
44
+ * own, not part of the protocol.
45
+ */
46
+ class ContextOwnerEvent extends Event {
47
+ element;
48
+ constructor(element) {
49
+ super('fluixi-context-owner');
50
+ this.element = element;
51
+ }
52
+ }
53
+ /** Tell consumers under `element` that the owner tree above them changed. */
54
+ export function announceOwner(element) {
55
+ element.ownerDocument.dispatchEvent(new ContextOwnerEvent(element));
56
+ }
57
+ /**
58
+ * The same context. Identity first, as the protocol has it; then the Fluixi id, because
59
+ * `@fluixi/core`'s `createContext` wraps the reactive one in a second object with the same id.
60
+ */
61
+ function matches(key, context) {
62
+ return key === context || (typeof key === 'object' && key !== null && key.id === context.id);
63
+ }
64
+ function describe(context) {
65
+ return context.id.description ?? 'context';
66
+ }
67
+ /**
68
+ * Answer `context-request` for `context` from inside `host`. `value` is a value or an accessor;
69
+ * with an accessor, subscribed consumers are called again when it changes. Stops when the
70
+ * reactive scope it was called in ends: call it in `render()`.
71
+ */
72
+ export function provideContext(host, context, value) {
73
+ const read = typeof value === 'function' ? value : () => value;
74
+ const subscribers = new Map();
75
+ const onRequest = (event) => {
76
+ const request = event;
77
+ // The host asking for its own context wants the one above it.
78
+ if (!matches(request.context, context) || request.contextTarget === host)
79
+ return;
80
+ event.stopImmediatePropagation();
81
+ if (!request.subscribe) {
82
+ request.callback(untrack(read));
83
+ return;
84
+ }
85
+ const unsubscribe = () => void subscribers.delete(request.callback);
86
+ subscribers.set(request.callback, unsubscribe);
87
+ request.callback(untrack(read), unsubscribe);
88
+ };
89
+ // A server render has no DOM events: the owner tree below carries the value there.
90
+ const dom = typeof host.addEventListener === 'function';
91
+ if (dom)
92
+ host.addEventListener('context-request', onRequest);
93
+ // Fluixi components the element renders read it with `useContext`, from the owner `render()`
94
+ // runs under.
95
+ const owner = getOwner();
96
+ let first = true;
97
+ const stopTracking = effect(() => {
98
+ const next = read();
99
+ if (owner)
100
+ (owner.contexts ??= new Map()).set(context.id, next);
101
+ if (first) {
102
+ first = false;
103
+ return;
104
+ }
105
+ for (const [callback, unsubscribe] of subscribers)
106
+ callback(next, unsubscribe);
107
+ }, undefined, { name: `provide ${describe(context)}` });
108
+ // After the render that called this has placed its content: a light DOM provider takes its
109
+ // children out while it renders, so a consumer among them is not inside it yet.
110
+ if (dom) {
111
+ queueMicrotask(() => {
112
+ if (!stopped)
113
+ host.dispatchEvent(new ContextProviderEvent(context, host));
114
+ });
115
+ }
116
+ let stopped = false;
117
+ const stop = () => {
118
+ if (stopped)
119
+ return;
120
+ stopped = true;
121
+ if (dom)
122
+ host.removeEventListener('context-request', onRequest);
123
+ stopTracking();
124
+ subscribers.clear();
125
+ };
126
+ onCleanup(stop);
127
+ return stop;
128
+ }
129
+ /**
130
+ * The nearest value of `context` above `host`, as an accessor.
131
+ *
132
+ * Asked of the DOM first. With no provider there, it is the value in the owner tree the element
133
+ * was created in, so `<Ctx.Provider>` in a Fluixi app reaches an element rendered inside it, and
134
+ * otherwise the context's default. A provider that appears later takes over. Stops when the
135
+ * reactive scope it was called in ends: call it in `render()`.
136
+ */
137
+ export function consumeContext(host, context, options = {}) {
138
+ const subscribe = options.subscribe ?? true;
139
+ // The owner tree as it stands now, and again if hydration links this element up later.
140
+ const owner = getOwner();
141
+ const fromTree = () => runWithOwner(owner, () => useContext(context));
142
+ const value = signal(fromTree(), { name: describe(context) });
143
+ // A server render: no DOM to ask, the owner tree is the answer.
144
+ if (typeof host.dispatchEvent !== 'function')
145
+ return () => value();
146
+ let answered = false;
147
+ let unsubscribe;
148
+ const ask = () => {
149
+ host.dispatchEvent(new ContextRequestEvent(context, host, (next, stop) => {
150
+ answered = true;
151
+ if (stop)
152
+ unsubscribe = stop;
153
+ // `.set` calls a function argument as an updater; a context can hold a function.
154
+ value.set(() => next);
155
+ }, subscribe));
156
+ };
157
+ const doc = host.ownerDocument;
158
+ const onProvider = (event) => {
159
+ const announced = event;
160
+ if (answered || !matches(announced.context, context))
161
+ return;
162
+ if (containsComposed(announced.contextTarget, host))
163
+ ask();
164
+ };
165
+ const onOwner = (event) => {
166
+ if (answered || !containsComposed(event.element, host))
167
+ return;
168
+ const next = fromTree();
169
+ value.set(() => next);
170
+ };
171
+ ask();
172
+ if (!answered) {
173
+ doc.addEventListener('context-provider', onProvider);
174
+ doc.addEventListener('fluixi-context-owner', onOwner);
175
+ }
176
+ onCleanup(() => {
177
+ unsubscribe?.();
178
+ doc.removeEventListener('context-provider', onProvider);
179
+ doc.removeEventListener('fluixi-context-owner', onOwner);
180
+ });
181
+ return () => value();
182
+ }
183
+ /** Whether `node` sits under `container` once shadow roots are looked through. */
184
+ function containsComposed(container, node) {
185
+ let at = node;
186
+ while (at) {
187
+ if (at === container)
188
+ return true;
189
+ at = at.parentNode ?? (at instanceof ShadowRoot ? at.host : null);
190
+ }
191
+ return false;
192
+ }
@@ -0,0 +1,141 @@
1
+ // src/context.ts
2
+ import { effect, getOwner, onCleanup, runWithOwner, signal, untrack, useContext } from "@fluixi/reactive/signal";
3
+ var ContextRequestEvent = class extends Event {
4
+ constructor(context, contextTarget, callback, subscribe) {
5
+ super("context-request", { bubbles: true, composed: true });
6
+ this.context = context;
7
+ this.contextTarget = contextTarget;
8
+ this.callback = callback;
9
+ this.subscribe = subscribe;
10
+ }
11
+ };
12
+ var ContextProviderEvent = class extends Event {
13
+ constructor(context, contextTarget) {
14
+ super("context-provider", { bubbles: true, composed: true });
15
+ this.context = context;
16
+ this.contextTarget = contextTarget;
17
+ }
18
+ };
19
+ var ContextOwnerEvent = class extends Event {
20
+ constructor(element) {
21
+ super("fluixi-context-owner");
22
+ this.element = element;
23
+ }
24
+ };
25
+ function announceOwner(element) {
26
+ element.ownerDocument.dispatchEvent(new ContextOwnerEvent(element));
27
+ }
28
+ function matches(key, context) {
29
+ return key === context || typeof key === "object" && key !== null && key.id === context.id;
30
+ }
31
+ function describe(context) {
32
+ return context.id.description ?? "context";
33
+ }
34
+ function provideContext(host, context, value) {
35
+ const read = typeof value === "function" ? value : () => value;
36
+ const subscribers = /* @__PURE__ */ new Map();
37
+ const onRequest = (event) => {
38
+ const request = event;
39
+ if (!matches(request.context, context) || request.contextTarget === host) return;
40
+ event.stopImmediatePropagation();
41
+ if (!request.subscribe) {
42
+ request.callback(untrack(read));
43
+ return;
44
+ }
45
+ const unsubscribe = () => void subscribers.delete(request.callback);
46
+ subscribers.set(request.callback, unsubscribe);
47
+ request.callback(untrack(read), unsubscribe);
48
+ };
49
+ const dom = typeof host.addEventListener === "function";
50
+ if (dom) host.addEventListener("context-request", onRequest);
51
+ const owner = getOwner();
52
+ let first = true;
53
+ const stopTracking = effect(
54
+ () => {
55
+ const next = read();
56
+ if (owner) (owner.contexts ??= /* @__PURE__ */ new Map()).set(context.id, next);
57
+ if (first) {
58
+ first = false;
59
+ return;
60
+ }
61
+ for (const [callback, unsubscribe] of subscribers) callback(next, unsubscribe);
62
+ },
63
+ void 0,
64
+ { name: `provide ${describe(context)}` }
65
+ );
66
+ if (dom) {
67
+ queueMicrotask(() => {
68
+ if (!stopped) host.dispatchEvent(new ContextProviderEvent(context, host));
69
+ });
70
+ }
71
+ let stopped = false;
72
+ const stop = () => {
73
+ if (stopped) return;
74
+ stopped = true;
75
+ if (dom) host.removeEventListener("context-request", onRequest);
76
+ stopTracking();
77
+ subscribers.clear();
78
+ };
79
+ onCleanup(stop);
80
+ return stop;
81
+ }
82
+ function consumeContext(host, context, options = {}) {
83
+ const subscribe = options.subscribe ?? true;
84
+ const owner = getOwner();
85
+ const fromTree = () => runWithOwner(owner, () => useContext(context));
86
+ const value = signal(fromTree(), { name: describe(context) });
87
+ if (typeof host.dispatchEvent !== "function") return () => value();
88
+ let answered = false;
89
+ let unsubscribe;
90
+ const ask = () => {
91
+ host.dispatchEvent(
92
+ new ContextRequestEvent(
93
+ context,
94
+ host,
95
+ (next, stop) => {
96
+ answered = true;
97
+ if (stop) unsubscribe = stop;
98
+ value.set(() => next);
99
+ },
100
+ subscribe
101
+ )
102
+ );
103
+ };
104
+ const doc = host.ownerDocument;
105
+ const onProvider = (event) => {
106
+ const announced = event;
107
+ if (answered || !matches(announced.context, context)) return;
108
+ if (containsComposed(announced.contextTarget, host)) ask();
109
+ };
110
+ const onOwner = (event) => {
111
+ if (answered || !containsComposed(event.element, host)) return;
112
+ const next = fromTree();
113
+ value.set(() => next);
114
+ };
115
+ ask();
116
+ if (!answered) {
117
+ doc.addEventListener("context-provider", onProvider);
118
+ doc.addEventListener("fluixi-context-owner", onOwner);
119
+ }
120
+ onCleanup(() => {
121
+ unsubscribe?.();
122
+ doc.removeEventListener("context-provider", onProvider);
123
+ doc.removeEventListener("fluixi-context-owner", onOwner);
124
+ });
125
+ return () => value();
126
+ }
127
+ function containsComposed(container, node) {
128
+ let at = node;
129
+ while (at) {
130
+ if (at === container) return true;
131
+ at = at.parentNode ?? (at instanceof ShadowRoot ? at.host : null);
132
+ }
133
+ return false;
134
+ }
135
+ export {
136
+ ContextProviderEvent,
137
+ ContextRequestEvent,
138
+ announceOwner,
139
+ consumeContext,
140
+ provideContext
141
+ };