@excom/neutron 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/.rush/temp/chunked-rush-logs/neutron.apply-exports.chunks.jsonl +1 -0
  2. package/.rush/temp/chunked-rush-logs/neutron.build_package-metas.chunks.jsonl +1 -0
  3. package/.rush/temp/operation/apply-exports/all.log +1 -0
  4. package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
  5. package/.rush/temp/operation/apply-exports/state.json +3 -0
  6. package/.rush/temp/operation/build_package-metas/all.log +1 -0
  7. package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
  8. package/.rush/temp/operation/build_package-metas/state.json +3 -0
  9. package/.rush/temp/shrinkwrap-deps.json +3 -0
  10. package/config/rig.json +6 -0
  11. package/index.ts +9 -0
  12. package/package.json +45 -0
  13. package/rush-logs/neutron.apply-exports.cache.log +1 -0
  14. package/rush-logs/neutron.apply-exports.log +1 -0
  15. package/rush-logs/neutron.build_package-metas.cache.log +1 -0
  16. package/rush-logs/neutron.build_package-metas.log +1 -0
  17. package/src/command.ts +102 -0
  18. package/src/common-element.ts +377 -0
  19. package/src/constants.ts +101 -0
  20. package/src/devtools-hook.ts +93 -0
  21. package/src/lifecycle-configs.ts +304 -0
  22. package/src/neutron-element.ts +72 -0
  23. package/src/neutron-error.ts +6 -0
  24. package/src/neutron-internal.ts +550 -0
  25. package/src/neutron.ts +36 -0
  26. package/src/types/effect.types.ts +104 -0
  27. package/src/types/element.types.ts +263 -0
  28. package/src/types/index.ts +4 -0
  29. package/src/types/new.types.ts +159 -0
  30. package/src/types/shared.types.ts +25 -0
  31. package/src/utils/effect.ts +357 -0
  32. package/src/utils/element.ts +382 -0
  33. package/src/utils/index.ts +2 -0
  34. package/support/docs/COMMANDS.md +58 -0
  35. package/support/docs/COMPOSE.md +32 -0
  36. package/support/docs/DEBUG.md +11 -0
  37. package/support/docs/DEFINE.md +20 -0
  38. package/support/docs/EFFECTS.md +49 -0
  39. package/support/docs/EVENTS.md +57 -0
  40. package/support/docs/LIFECYCLES.md +69 -0
  41. package/support/docs/METHODS.md +49 -0
  42. package/support/docs/PROMISE_PROPS.md +29 -0
  43. package/support/docs/PROPS.md +64 -0
  44. package/support/docs/PROP_REACTIONS.md +35 -0
  45. package/support/docs/PROVISION.md +31 -0
  46. package/support/docs/README.md +118 -0
  47. package/support/docs/RECOMPOSE.md +70 -0
  48. package/support/docs/TYPESCRIPT.md +51 -0
  49. package/support/docs-sections.json +42 -0
  50. package/support/package-meta.json +129 -0
  51. package/support/tests/commands.test.ts +330 -0
  52. package/support/tests/common-element.test.ts +342 -0
  53. package/support/tests/devtools-hook.test.ts +209 -0
  54. package/support/tests/devtools-renderer.test.ts +125 -0
  55. package/support/tests/effects.test.ts +253 -0
  56. package/support/tests/element-config.test.ts +331 -0
  57. package/support/tests/entry.test.ts +68 -0
  58. package/support/tests/lifecycles.test.ts +489 -0
  59. package/support/tests/loop-guard.test.ts +162 -0
  60. package/support/tests/neutron.test.ts +1286 -0
  61. package/support/tests/recompose.test.ts +129 -0
  62. package/support/tests/utils.test.ts +75 -0
  63. package/tsconfig.json +5 -0
@@ -0,0 +1,69 @@
1
+ # Lifecycles
2
+
3
+ Chain `.on*` handlers that return effects; Neutron applies them, tracks listeners, and routes errors.
4
+
5
+ ## Handlers
6
+
7
+ Each handler receives the element first, then any lifecycle-specific argument, and returns an [effect](./EFFECTS.md). Prefer effects over mutating the element directly. Every `on*` has an `off*` twin that unregisters the same function.
8
+
9
+ ```ts
10
+ Neutron({ tag: "panel-host", props: { isReady: Boolean } })
11
+ .onConstructed(() => ({ /* runs in the constructor, before connect */ }))
12
+ .onConnected(() => ({
13
+ // every connect, including reconnects
14
+ isReady: true,
15
+ emit: ["panel-host-ready"],
16
+ }))
17
+ .onDisconnected((el) => ({
18
+ // tear down; `el.isMoving` is true when a disconnect is followed by a connect in the same tick
19
+ isReady: false,
20
+ }))
21
+ .onAdopted(() => ({}))
22
+ .onError((_el, err) => {
23
+ console.error(err);
24
+ });
25
+ ```
26
+
27
+ Disconnect is settled one microtask after `disconnectedCallback`. If the element is re-inserted before then (a DOM move), `onDisconnected` and `onConnected` both still run, with `isMoving` set. An error thrown by any handler is routed to `onError(el, error)`; without an `onError`, it is rethrown.
28
+
29
+ Reactions to prop changes, events, commands and promises are their own pages: [Prop reactions](./PROP_REACTIONS.md), [Events](./EVENTS.md), [Commands](./COMMANDS.md), [Promise props](./PROMISE_PROPS.md).
30
+
31
+ ## Destructure the element argument
32
+
33
+ Prefer `({ prop }) => …` over `(el) => …`. A handler that only ever sees the values it names cannot reach for `el.setAttribute`, `el.querySelector(…).value = …` or any other imperative mutation — reading the signature is enough to know the handler is pure, and the effect it returns is the whole story.
34
+
35
+ ```ts
36
+ Neutron({ tag: "price-tag", props: { amount: Number, currency: String } })
37
+ .onPropChanged(["amount", "currency"], ({ amount, currency }) => ({
38
+ ariaLabel: `${amount} ${currency}`,
39
+ }));
40
+ ```
41
+
42
+ ## Pitfall: stale values in async callbacks
43
+
44
+ Destructuring copies the values at call time. If the handler starts asynchronous work and the callback reads those copies, it sees the element as it was when the work started, not when it finished:
45
+
46
+ ```ts
47
+ // ✗ `amount` here is whatever it was when the fetch began
48
+ .onConnected(({ apiUrl, amount }) => ({
49
+ addListener: ["price-tag-refresh", () => fetch(apiUrl).then(() => console.log(amount))],
50
+ }))
51
+ ```
52
+
53
+ Make the callback a `defineMethods` method instead. [Methods](./METHODS.md) are effectors too — Neutron calls them with the live element first — so the callback destructures fresh values when it actually runs, and its return value is applied as an effect. The lifecycle keeps `el` only to reach the method (methods are bound to the element; call them as `el.method(…)`):
54
+
55
+ ```ts
56
+ Neutron({ tag: "price-tag", props: { apiUrl: String, amount: Number } })
57
+ .defineMethods({
58
+ // runs later, with the element as it is *then*
59
+ applyQuote: ({ amount }, quote: { rate: number }) => ({
60
+ amount: amount * quote.rate,
61
+ }),
62
+ })
63
+ .onConnected((el) => ({
64
+ addListener: [
65
+ "price-tag-refresh",
66
+ () => fetch(el.apiUrl).then((r) => r.json()).then((quote) => el.applyQuote(quote)),
67
+ ],
68
+ }));
69
+ ```
@@ -0,0 +1,49 @@
1
+ # Methods
2
+
3
+ Methods are effectors like lifecycles: they receive the live element, return an effect, and can hand a value back to the caller.
4
+
5
+ ## Defining methods
6
+
7
+ ```ts
8
+ Neutron({ tag: "tally-counter", props: { tallyCount: Number } })
9
+ .defineMethods({
10
+ increment: ({ tallyCount }, step = 1) => ({
11
+ tallyCount: (tallyCount ?? 0) + step,
12
+ returns: (tallyCount ?? 0) + step, // value returned to the caller
13
+ }),
14
+ })
15
+ .define();
16
+
17
+ // el.increment(2) → number
18
+ ```
19
+
20
+ Element first, then the call arguments. Other effects can call them by name: `increment: [2]`. Because Neutron calls a method with the element as it is *then*, methods are also the right shape for async callbacks — see [Lifecycles](./LIFECYCLES.md#md-pitfall-stale-values-in-async-callbacks).
21
+
22
+ ### Pre-declaring signatures
23
+
24
+ `defineMethods` types `element` as the element *before* that call, so a method cannot reach a sibling defined in the same object. `withTypes<T>()` is a type-only step (no runtime effect) that puts the signatures on the element type first — methods are bound and the element argument is stripped, so declare them as the element sees them. It also makes `typeof Builder.CustomElement` usable for module-level helpers.
25
+
26
+ ```ts
27
+ interface Methods {
28
+ flush: FrameRequestCallback;
29
+ schedule: () => void;
30
+ }
31
+
32
+ export const Ticker = Neutron({ tag: "tick-er", props: { tickCount: Number } })
33
+ .withTypes<Methods>();
34
+
35
+ type El = typeof Ticker.CustomElement;
36
+
37
+ const label = (el: El) => `${el.tickCount}`;
38
+
39
+ Ticker.defineMethods({
40
+ flush: ({ tickCount }) => ({ tickCount: tickCount + 1 }),
41
+ schedule: ({ flush }) => {
42
+ requestAnimationFrame(flush); // sibling method, no cast
43
+ },
44
+ }).define();
45
+ ```
46
+
47
+ ## Keep the imperative surface small
48
+
49
+ If a method is something the consuming app may want to invoke, accept it as a command (`onCommand("--verb")`, see [Commands](./COMMANDS.md)); if it is something the app may want to cancel, fire an event with a default action (`onEventDefault`, see [Events](./EVENTS.md)). Neither needs a public method.
@@ -0,0 +1,29 @@
1
+ # Promise props
2
+
3
+ Store a promise in a prop and react to its settlement; replacing it cancels the stale one.
4
+
5
+ ## Resolve / reject handlers
6
+
7
+ ```ts
8
+ Neutron({
9
+ tag: "lazy-item",
10
+ props: {
11
+ srcPromise: Promise,
12
+ provision: Object,
13
+ },
14
+ })
15
+ .onConnected(() => ({
16
+ srcPromise: fetch("/api/item").then((r) => r.json()),
17
+ }))
18
+ .onPromiseResolved("srcPromise", (_el, result) => ({
19
+ srcPromise: null,
20
+ provision: result.srcPromise,
21
+ }))
22
+ .onPromiseRejected("srcPromise", () => ({ srcPromise: null }));
23
+ ```
24
+
25
+ `result` maps the prop name to the resolved value (or the rejection reason).
26
+
27
+ ## Cancellation
28
+
29
+ Assigning a new promise or `null` while the previous one is pending cancels it — the stale settlement never reaches the handlers.
@@ -0,0 +1,64 @@
1
+ # Props
2
+
3
+ Declare an element's state once; primitives become dashed attributes that CSS and Quark select on, rich values stay on the instance.
4
+
5
+ ## Declaring props
6
+
7
+ Shorthand constructors reflect primitives to dashed attributes. Rich config adds defaults, validation, storage, and custom serialize / deserialize.
8
+
9
+ ```ts
10
+ import { Neutron, TokenList } from "@excom/neutron";
11
+
12
+ Neutron({
13
+ tag: "usage-meter",
14
+ props: {
15
+ unitLabel: String, // reflects ↔ `unit-label`
16
+ maxCount: Number, // reflects ↔ `max-count`
17
+ isOpen: Boolean, // presence attribute `is-open`
18
+ featureTags: TokenList, // space-separated tokens ↔ `feature-tags`, read as `string[]`
19
+ // non-reflecting by default:
20
+ payload: Object,
21
+ items: Array,
22
+ srcPromise: Promise,
23
+ // rich config:
24
+ maxValue: {
25
+ type: Number,
26
+ defaultValue: () => 100,
27
+ isValid: (n) => n > 0,
28
+ },
29
+ // element references are always weak — never pin another node:
30
+ inputEl: { type: HTMLInputElement, store: "weak" },
31
+ },
32
+ });
33
+ ```
34
+
35
+ ## Rich config keys
36
+
37
+ | Key | Purpose |
38
+ | --- | --- |
39
+ | `type` | Constructor. `String` / `Number` / `Boolean` / `TokenList` reflect to an attribute; everything else is instance-only. |
40
+ | `defaultValue` | `() => value`, returned when the prop is nullish or invalid. |
41
+ | `isValid` | `(value) => boolean`. Invalid values fall back to the default; for `TokenList` the invalid tokens are filtered out instead. |
42
+ | `attr` | Override the attribute name, or `false` to keep a primitive off the attribute. |
43
+ | `store` | `"weak"` holds the value in a `WeakRef` and derefs on read. Required for every prop that references another element, so a removed node can be collected. |
44
+ | `serialize` / `deserialize` | Transform on write / read. |
45
+
46
+ ## TokenList
47
+
48
+ `TokenList` is exported by this package. It marks a space-separated attribute (`feature-tags="a b"`) whose property value is a plain `string[]` — the element-side equivalent of `class`. Prefer it over `Array` whenever the list belongs in the document, so CSS and Quark can select on it (`usage-meter[feature-tags~="a"]`).
49
+
50
+ ## Built-in instance props
51
+
52
+ `isMounted`, `isMoving`, `isAdopted`, `wasMounted` exist on every element. They are instance-only; list them in `reflectDefaultProps: ["isMounted"]` to reflect them as attributes (`is-mounted`).
53
+
54
+ ## Naming rules
55
+
56
+ Enforced at definition time or by convention:
57
+
58
+ - Custom attributes must contain a dash (`max-count`, `is-open`), so they can never collide with a native attribute — now or in the future. Dev mode warns on dash-less, `data-*`, and `aria-*` attributes.
59
+ - Booleans read as assertions: `is-loading`, `did-fail`, `has-rendered`, `should-fetch`.
60
+ - Events are tag-prefixed (`press-tracker-press`), never bare (`change`).
61
+ - Attribute names may not start with `q-`, `n-`, `on-`, or `off-` (reserved by Quark and Neutron).
62
+ - Prop names may not shadow Neutron internals or effect keywords (`returns`, `content`, lifecycle names, `_n_`, `_q_`).
63
+ - Private state and methods take a leading underscore.
64
+ - Loosely couple: element-typed props use `store: "weak"`, and anything else that holds a node is cleared in `onDisconnected`.
@@ -0,0 +1,35 @@
1
+ # Prop reactions
2
+
3
+ React to one prop, or a batch of them, with the previous values in hand.
4
+
5
+ ## Handlers
6
+
7
+ ```ts
8
+ Neutron({ tag: "echo-field", props: { fieldValue: String, isFilled: Boolean } })
9
+ .onPropSet("fieldValue", () => ({
10
+ /* fieldValue became truthy for its type */
11
+ isFilled: true,
12
+ }))
13
+ .onPropUnset("fieldValue", () => ({
14
+ /* fieldValue became falsy / removed */
15
+ isFilled: false,
16
+ }))
17
+ .onPropChanged("fieldValue", ({ fieldValue }, previous) => ({
18
+ /* any change, including unset → set; `previous.fieldValue` is the old value */
19
+ emit: ["echo-field-change", { detail: { fieldValue, previous: previous.fieldValue } }],
20
+ }))
21
+ // one handler for a batch of props — runs when any of them changed
22
+ .onEffect(["fieldValue", "isFilled"], (el, previous) => ({
23
+ emit: ["echo-field-effect", { detail: { previous } }],
24
+ }));
25
+ ```
26
+
27
+ `onPropChanged` and `onEffect` take one name or an array of names. The second argument maps every prop that changed in the batch to its **previous** value.
28
+
29
+ ## Mount gating
30
+
31
+ Reactions run only on mounted elements. Changes made before the first connect (attributes parsed from HTML, props set on a detached element) are kept and flushed as one batch on first mount, so reaction handlers always see the settled initial state.
32
+
33
+ ## No self-writes
34
+
35
+ A handler must not set the same prop it is reacting to — Neutron throws a `NeutronError`. Child effects on element-typed props are the exception, so `.onPropSet("inputEl", () => ({ inputEl: { addListener: [...] } }))` is allowed.
@@ -0,0 +1,31 @@
1
+ # Provision
2
+
3
+ One property carries everything rich an element publishes to the document, so Quark and app JS have a single place to read it.
4
+
5
+ ## Declaring a provision
6
+
7
+ All public, rich data an element exposes to the document goes through one property: `provision`. Declare it as `provision: Object` (or a typed constructor), set it from an effect, and tag it `@provision` in JSDoc.
8
+
9
+ ```ts
10
+ Neutron({
11
+ tag: "provider-ping",
12
+ props: { provision: Object },
13
+ })
14
+ .onConnected(() => ({ provision: { at: Date.now() } }))
15
+ .define();
16
+ ```
17
+
18
+ ## Reading it
19
+
20
+ Every set emits `neutron-provision` for app JS. Quark reads it on the element itself with `prop("provision")` and re-runs when it is assigned — assign a new object rather than mutating the old one.
21
+
22
+ ```quark
23
+ provider-ping {
24
+ $ping: prop("provision");
25
+ [bind-at] { content: $ping.at; }
26
+ }
27
+ ```
28
+
29
+ ## Ordering
30
+
31
+ Within one [effect](./EFFECTS.md), `provision` is always applied last, so listeners of `neutron-provision` see every other prop already settled.
@@ -0,0 +1,118 @@
1
+ # neutron
2
+
3
+ Define typed custom elements with effect-based lifecycles — props, events, and compose without rewriting the Custom Elements boilerplate.
4
+
5
+ Neutron is the element factory of the Nucleus Stack: `Neutron({ tag, props })` returns a builder you chain lifecycles onto, then `define()`. Every Nucleus Kit element is a Neutron element, and so is every element you write yourself.
6
+
7
+ ## Features
8
+
9
+ - **Declarative factory** `Neutron({ tag, props }).… .define()`
10
+ - **Typed props** Primitives and `TokenList` reflect to dashed attributes; objects / arrays / elements / promises stay on the instance
11
+ - **Effect returns** Lifecycles / methods return a POJO (or an array of them) that sets props, emits, listens, calls methods, and styles
12
+ - **Fine-grained reactions** `onPropSet` / `Unset` / `Changed` / `onEffect`
13
+ - **Events & broadcasts** Tag-prefixed custom events, cancelable default actions, channel broadcasts
14
+ - **Commands** `onCommand("--verb")` handles the HTML Command API — `<button command commandfor>` needs no custom element
15
+ - **Listener cleanup** Listeners added through effects are removed on disconnect and restored on reconnect
16
+ - **Compose** Combine builders (`Neutron.compose`) for mixin-style packages
17
+ - **Recompose** Import a package's raw builder, add / remove lifecycles and methods, then `define()` it yourself
18
+ - **DevTools** `Neutron.attachDevtools()` hooks the Nucleus DevTools extension
19
+
20
+ ## Installation
21
+
22
+ <include-content is-active template-ref="/views/install-section/install-section.html"></include-content>
23
+
24
+ ## Usage
25
+
26
+ An element owns its own state (attributes) and announces changes (events). It never renders children or reaches into siblings — coordination belongs to Quark. The rules these examples follow are collected in [Best Practices](/nucleus/docs/best_practices) and [Creating Elements](/nucleus/docs/creating_elements).
27
+
28
+ ```ts
29
+ import { Neutron } from "@excom/neutron";
30
+
31
+ export const PressTracker = Neutron({
32
+ tag: "press-tracker",
33
+ props: {
34
+ pressCount: { type: Number, defaultValue: () => 0 }, // reflects ↔ `press-count`
35
+ },
36
+ })
37
+ .onEvent("click", ({ pressCount }) => ({
38
+ // effects are declarative instructions, not imperative mutations
39
+ pressCount: pressCount + 1,
40
+ emit: ["press-tracker-press", { detail: { pressCount: pressCount + 1 } }],
41
+ }));
42
+
43
+ PressTracker.define();
44
+ ```
45
+
46
+ ```html
47
+ <press-tracker press-count="0">
48
+ <button>Press</button>
49
+ </press-tracker>
50
+ <!-- `press-tracker[press-count="3"]` is now a CSS / Quark selector -->
51
+ ```
52
+
53
+ ### Documentation
54
+
55
+ Defining elements
56
+
57
+ - [Props](./PROPS.md) — typed props, reflection, `TokenList`, naming rules
58
+ - [Provision](./PROVISION.md) — the one property for published rich data
59
+ - [TypeScript](./TYPESCRIPT.md) — global element types, `ConstructorType`
60
+
61
+ Behavior
62
+
63
+ - [Lifecycles](./LIFECYCLES.md) — `onConnected` & co., destructuring, async pitfalls
64
+ - [Effects](./EFFECTS.md) — the object a handler returns
65
+ - [Prop reactions](./PROP_REACTIONS.md) — `onPropSet` / `Unset` / `Changed` / `onEffect`
66
+ - [Methods](./METHODS.md) — methods as effectors
67
+ - [Events](./EVENTS.md) — emits, default actions, broadcasts, listener cleanup
68
+ - [Commands](./COMMANDS.md) — `onCommand` for `--verb` commands, the `command` effect
69
+ - [Promise props](./PROMISE_PROPS.md) — `onPromiseResolved` / `Rejected`
70
+
71
+ Composition
72
+
73
+ - [Compose](./COMPOSE.md) — stack builders into mixin-style packages
74
+ - [Recompose](./RECOMPOSE.md) — edit a packaged element before defining it
75
+
76
+ Runtime
77
+
78
+ - [Define](./DEFINE.md) — `define()` and class introspection
79
+ - [Debug](./DEBUG.md) — DevTools hook, loop guard
80
+
81
+ ### Examples
82
+
83
+ #### State on connect
84
+
85
+ ```ts
86
+ Neutron({ tag: "ready-flag", props: { isReady: Boolean } })
87
+ .onConnected(() => ({ isReady: true, emit: ["ready-flag-ready"] }))
88
+ .define();
89
+ ```
90
+
91
+ #### Child element effect across handlers
92
+
93
+ Assign an element prop, then react to it with a nested effect. Listener callbacks that return effects must be `defineMethods` methods:
94
+
95
+ ```ts
96
+ Neutron({
97
+ tag: "focus-host",
98
+ props: {
99
+ inputEl: { type: HTMLInputElement, store: "weak" },
100
+ isFocused: Boolean,
101
+ },
102
+ })
103
+ .defineMethods({
104
+ handleFocus: () => ({ isFocused: true, emit: ["focus-host-focus"] }),
105
+ handleBlur: () => ({ isFocused: false }),
106
+ })
107
+ .onConnected((el) => ({
108
+ inputEl: el.querySelector("input"),
109
+ }))
110
+ .onPropSet("inputEl", ({ handleFocus, handleBlur }) => ({
111
+ inputEl: {
112
+ addListeners: [
113
+ ["focus", handleFocus],
114
+ ["blur", handleBlur],
115
+ ],
116
+ },
117
+ }));
118
+ ```
@@ -0,0 +1,70 @@
1
+ # Recompose
2
+
3
+ A builder is open until `define()` runs: import a package's raw builder, edit it, then register it yourself.
4
+
5
+ ## Import the raw builder
6
+
7
+ Every element package ships two entries: `index.ts` (calls `.define()` and registers the global types) and the raw element source (`<element>.ts`), which only exports the builder. Import the raw entry, edit it, then register it yourself:
8
+
9
+ ```ts
10
+ // app.ts — never import "@excom/detect-browser" (or nucleus-kit) here: that entry defines the element immediately
11
+ import { DetectBrowser } from "@excom/detect-browser/detect-browser";
12
+
13
+ DetectBrowser
14
+ // add lifecycles / methods the package did not ship
15
+ .onPropSet("isStandalone", () => ({ emit: ["detect-browser-standalone"] }))
16
+ .defineMethods({ clearInfo: () => ({ provision: null }) })
17
+ .define();
18
+ ```
19
+
20
+ Most recomposition is exactly that: chaining new lifecycles onto a packaged element. Unregistering is rarer and goes through `builtConfig`.
21
+
22
+ ## builtConfig
23
+
24
+ Every builder exposes its definition as `builtConfig`, a plain object you can read before `define()`:
25
+
26
+ | Key | Shape |
27
+ | --- | --- |
28
+ | `tag` | The packaged tag name (also the event prefix). |
29
+ | `props` | `{ [propName]: PropConfig }` — the normalized prop configs (`type`, `attr`, `defaultValue`, …). |
30
+ | `events` / `broadcasts` | `{ [name]: { prefixWithTag? } }` |
31
+ | `methods` | `[name, fn][]` in definition order. |
32
+ | `lifecycles` | `{ constructed, connected, adopted, disconnected, error, effect, propSet, propUnset, propChanged, promiseResolved, promiseRejected, broadcast, event, eventDefault }`, each an array of `[names, handler]` pairs in registration order. `names` is the prop / event list the handler was registered with (`[]` for lifecycles without one). |
33
+ | `reflectDefaultProps` / `definitionOpts` | As passed to `Neutron()`. |
34
+
35
+ ## Removing a lifecycle
36
+
37
+ Removal needs the original handler reference, because `off*` matches by function identity. Index into `builtConfig.lifecycles.<lifecycle>[entry][1]` and pass that handler to the matching `off*`:
38
+
39
+ ```ts
40
+ // stop detecting on connect; the app calls `el.setBrowserInfo()` when it wants to
41
+ DetectBrowser.offConnected(DetectBrowser.builtConfig.lifecycles.connected[0][1]);
42
+
43
+ // named lifecycles: pass the names too — only those names are detached, the handler keeps any others
44
+ DetectBrowser.offPropSet("isStandalone", DetectBrowser.builtConfig.lifecycles.propSet[0][1]);
45
+ ```
46
+
47
+ Indexing by position is intentional for now (a friendlier handle may come later); read `builtConfig.lifecycles.<lifecycle>` once to see which entry you are after.
48
+
49
+ ## Adding props
50
+
51
+ Adding props goes through [`Neutron.compose`](./COMPOSE.md), which also leaves the imported builder untouched (it deep-clones):
52
+
53
+ ```ts
54
+ import { Neutron } from "@excom/neutron";
55
+
56
+ export const StampedDetectBrowser = Neutron.compose([
57
+ DetectBrowser,
58
+ Neutron({ tag: "detect-browser", props: { detectedAt: String } }),
59
+ ]).onPropSet("provision", () => ({ detectedAt: new Date().toISOString() }));
60
+
61
+ StampedDetectBrowser.define();
62
+ ```
63
+
64
+ ## Rules
65
+
66
+ - Everything must happen before `define()`. The runtime config is built at that moment; later `on*` / `off*` / `defineMethods` calls return the builder but change nothing.
67
+ - The raw builder is a module singleton — an in-place edit is visible to every importer. Use `Neutron.compose` when you want a modified copy instead.
68
+ - Added handlers run after the packaged ones for the same lifecycle (registration order) and batch into the same effect pass.
69
+ - `define("other-tag")` registers the class under a different name. Events configured with `prefixWithTag` keep the packaged prefix, because the prefix comes from the builder's `tag`, not from the registered name.
70
+ - Skipping `index.ts` also skips its `declare global` block — add your own (see [TypeScript](./TYPESCRIPT.md)) if you want the tag typed.
@@ -0,0 +1,51 @@
1
+ # TypeScript
2
+
3
+ Expose an element's inferred type the way the DOM exposes its own elements, so queries and `createElement` need no casts.
4
+
5
+ ## Global element types
6
+
7
+ `Neutron()` infers the element type from `props` and `defineMethods`. Publish it as a global `HTML*Element` interface plus an `HTMLElementTagNameMap` entry, so `document.querySelector("press-tracker")` and `document.createElement("press-tracker")` are typed without casts:
8
+
9
+ ```ts
10
+ // index.ts — the package entry defines and types the element
11
+ import { PressTracker } from "./press-tracker";
12
+
13
+ PressTracker.define();
14
+
15
+ export { PressTracker };
16
+
17
+ type T_HTMLPressTrackerElement = typeof PressTracker.CustomElement;
18
+ declare global {
19
+ interface HTMLPressTrackerElement extends T_HTMLPressTrackerElement {}
20
+ interface Window {
21
+ HTMLPressTrackerElement: HTMLPressTrackerElement;
22
+ }
23
+ interface HTMLElementTagNameMap {
24
+ "press-tracker": HTMLPressTrackerElement;
25
+ }
26
+ }
27
+ export type { HTMLPressTrackerElement };
28
+ ```
29
+
30
+ `CustomElement` is a type-only handle on the builder (there is no runtime value); `Props` is the inferred props object.
31
+
32
+ ## Typed rich props
33
+
34
+ To type a rich prop more precisely than its constructor allows, cast the constructor with `ConstructorType<T>`:
35
+
36
+ ```ts
37
+ import { ConstructorType, Neutron } from "@excom/neutron";
38
+
39
+ type FeatureInfo = { fullSupport: string[]; noSupport: string[] };
40
+
41
+ Neutron({
42
+ tag: "detect-features",
43
+ props: {
44
+ provision: Object as unknown as ConstructorType<FeatureInfo>, // el.provision: FeatureInfo
45
+ },
46
+ });
47
+ ```
48
+
49
+ ## Event types
50
+
51
+ Document Neutron-emitted events with `TEvent` plus `type` and `detail` — see [Events](./EVENTS.md#md-typing-events).
@@ -0,0 +1,42 @@
1
+ {
2
+ "sections": [
3
+ {
4
+ "id": "defining-elements",
5
+ "title": "Defining Elements",
6
+ "docs": [
7
+ "props",
8
+ "provision",
9
+ "typescript"
10
+ ]
11
+ },
12
+ {
13
+ "id": "behavior",
14
+ "title": "Behavior",
15
+ "docs": [
16
+ "lifecycles",
17
+ "effects",
18
+ "prop_reactions",
19
+ "methods",
20
+ "events",
21
+ "commands",
22
+ "promise_props"
23
+ ]
24
+ },
25
+ {
26
+ "id": "composition",
27
+ "title": "Composition",
28
+ "docs": [
29
+ "compose",
30
+ "recompose"
31
+ ]
32
+ },
33
+ {
34
+ "id": "runtime",
35
+ "title": "Runtime",
36
+ "docs": [
37
+ "define",
38
+ "debug"
39
+ ]
40
+ }
41
+ ]
42
+ }