@symbiote-native/engine 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -2,7 +2,15 @@
2
2
  // Every framework adapter drives this tiny mutation API; all Fabric-specific
3
3
  // logic (tag allocation, view-name resolution, clone-on-write, event
4
4
  // normalization) lives behind it, in one place.
5
- export { createElement, createRawText, createAnchor, isAnchor, appendChild, insertBefore, removeChild, setProp, setEventListener, routeProp, censusRetainedTree, getExplicitStyle, setNodeHidden, setText, isSymbioteNode, isSymbioteEvent, RAW_TEXT_COMPONENT, debugNodeId, } from './node.js';
5
+ export { createElement, createRawText, createAnchor,
6
+ // The component name of a node the commit walk skips and whose children flatten into its parent.
7
+ // Exported so a PRIMITIVE that renders no view of its own can be born with it — RN's
8
+ // TouchableNativeFeedback clones onto its single child and commits nothing
9
+ // (TouchableNativeFeedback.js:339) — rather than being converted after the fact.
10
+ ANCHOR_COMPONENT, isAnchor, appendChild, insertBefore, removeChild, setProp, setEventListener, routeProp, censusRetainedTree, getExplicitStyle, setNodeHidden, setNodeComponent, setNodePressed, setText, isSymbioteNode, isSymbioteEvent, RAW_TEXT_COMPONENT, debugNodeId, } from './node.js';
11
+ // For a HOST BEHAVIOR that owns an animated style layer on its own node — TouchableOpacity's press
12
+ // fade, which RN drives from an `Animated.View` the tag replaces.
13
+ export { setAnimatedBehaviorStyle } from './animated/host-binding.js';
6
14
  export { isEventFor } from './view-config.js';
7
15
  export { registerComponent, setNativeViewConfigSource } from './registry.js';
8
16
  // Real cross-package consumer: core/components' KeyboardAvoidingView render narrows
@@ -24,9 +32,13 @@ export { setColorProcessor, processColor, dispatchViewCommand, sendAccessibility
24
32
  // across adapters has to hang off this, not off a per-framework lifecycle hook, or it measures a
25
33
  // different quantity in each one under the same name.
26
34
  export { registerPostCommit, unregisterPostCommit } from './post-commit.js';
27
- // The public instance grafted onto a host node by every adapter (React's getPublicInstance,
28
- // the Vue renderer's createElement): the imperative measure/setNativeProps/focus API. Lives
29
- // here because it depends only on engine internals, so all adapters inherit it identically.
35
+ // The aria/role -> accessibility* fold. Lives here rather than in a component wrapper because a
36
+ // LOWERED element has no wrapper: `fabricProps` runs it on the way to the payload, so every path
37
+ // gets it. `core/components`' typed `resolveAccessibilityProps` delegates to this one.
38
+ export { ARIA_ALIAS_KEYS, foldAriaProps } from './accessibility-props.js';
39
+ // The public instance every host node already is (React's getPublicInstance, the Vue renderer's
40
+ // createElement): the imperative measure/setNativeProps/focus API, on the shared node prototype.
41
+ // toPublicInstance is the identity that names the seam — see ./host-instance.
30
42
  export { toPublicInstance } from './host-instance/index.js';
31
43
  export { PlatformColor, DynamicColorIOS, isOpaqueColorValue, } from './platform-color/index.js';
32
44
  // CSS-style processors (boxShadow/filter): RN parses these in JS before native because
@@ -87,3 +99,10 @@ export { AccessibilityInfo } from './accessibility-info';
87
99
  // .ios re-export would otherwise duplicate-export the type symbols).
88
100
  export { applyStatusBarProps, statusBarImperative, statusBarCurrentHeight, } from './status-bar';
89
101
  export { hideTransition, STATUS_BAR_MANAGER, ANIMATED_HIDE_TRANSITION, STATIC_HIDE_TRANSITION, } from './status-bar/shared.js';
102
+ export { registerHostBehavior, hasHostBehaviors, hostBehaviorFor, clearHostBehaviors, appListenerFor, addDerivedNode, } from './host-behavior.js';
103
+ export { requestCommitFor } from './commit.js';
104
+ // `markPropsDirty` is a behavior's only way to say "the fold reads state I just changed". Every
105
+ // other dirtying route goes through a prop write, and a behavior whose payload is DERIVED — the
106
+ // sticky header's debounced translateY lives in its own runtime, not in `node.props` — has no
107
+ // prop to write. Pair it with `requestCommitFor`: dirtying is not publishing.
108
+ export { setBehaviorListener, markPropsDirty } from './node.js';
package/build/node.d.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import type { IFabricNode, IFabricProps, IRootTag, IMeasureOnSuccess, IMeasureInWindowOnSuccess, IMeasureLayoutOnSuccess } from './fabric';
2
+ import { type IClassNameValue } from './style-registry';
3
+ import { type IPayloadFold } from './host-behavior';
1
4
  declare const BRAND: unique symbol;
2
5
  export declare const RAW_TEXT_COMPONENT = "RCTRawText";
3
6
  export declare const TEXT_COMPONENT = "RCTText";
@@ -13,15 +16,68 @@ export type IListener = (event: ISymbioteEvent) => unknown;
13
16
  export declare function isSymbioteEvent(value: unknown): value is ISymbioteEvent;
14
17
  export interface ISymbioteNode {
15
18
  readonly [BRAND]: true;
16
- readonly component: string;
19
+ component: string;
17
20
  readonly isText: boolean;
18
21
  props: Record<string, unknown>;
19
22
  listeners: Map<string, IListener> | undefined;
20
23
  children: ISymbioteNode[];
21
24
  parent: ISymbioteNode | undefined;
22
25
  dirty: boolean;
26
+ propsDirty: boolean;
27
+ hasAriaAlias: boolean;
28
+ payloadFold: IPayloadFold | undefined;
29
+ structureDirty: boolean;
30
+ committed: IMirror | undefined;
31
+ styleParts: IClassStyleParts | undefined;
32
+ childHost: ISymbioteNode | undefined;
33
+ wrapper: ISymbioteNode | undefined;
34
+ measure(callback: IMeasureOnSuccess): void;
35
+ measureInWindow(callback: IMeasureInWindowOnSuccess): void;
36
+ measureLayout(relativeToNativeNode: ISymbioteNode | number, onSuccess: IMeasureLayoutOnSuccess, onFail?: () => void): void;
37
+ setNativeProps(nativeProps: Record<string, unknown>): void;
38
+ focus(): void;
39
+ blur(): void;
40
+ scrollTo(options?: {
41
+ x?: number;
42
+ y?: number;
43
+ animated?: boolean;
44
+ }): void;
45
+ scrollToEnd(options?: {
46
+ animated?: boolean;
47
+ }): void;
48
+ flashScrollIndicators(): void;
23
49
  }
24
- export declare function createElement(component: string, isText?: boolean): ISymbioteNode;
50
+ export interface IMirror {
51
+ handle: IFabricNode;
52
+ tag: number;
53
+ rootTag: IRootTag;
54
+ props: IFabricProps;
55
+ children: readonly ISymbioteNode[];
56
+ viewName: string;
57
+ parent: ISymbioteNode | undefined;
58
+ owner: ISymbioteNode;
59
+ }
60
+ /**
61
+ * The committed record for `node`, or `undefined` if it has never been committed - or if `node` is
62
+ * not the raw retained node at all.
63
+ *
64
+ * That second case is the reason this is a function rather than a bare `node.committed` read. The
65
+ * engine identifies a node BY IDENTITY, and the classic way to break that is to hand the engine a
66
+ * wrapper instead of the node: a Vue `reactive()`/deep-`ref()` Proxy around a host element is the
67
+ * one that actually happens (see the vue-adapter-reactivity skill; `shallowRef` is the fix).
68
+ *
69
+ * The old WeakMap caught this for free - a Proxy is a different object, so `mirror.get(proxy)` missed
70
+ * and every imperative API bailed with a clear "node not committed". A plain property read does NOT:
71
+ * a Proxy forwards `proxy.committed` straight to the target and hands back a real record, whose
72
+ * `handle` Vue would then deep-wrap on the way out. That handle is a JSI host object; a Proxy around
73
+ * it reaches `cloneNodeWithNewProps` and fails somewhere deep in native, far from the cause.
74
+ *
75
+ * So the identity check that was implicit in the WeakMap is explicit here: a record written on the
76
+ * raw node names it, and `record.owner !== node` means whatever we were handed is not that node.
77
+ * One reference comparison, and the wrap now fails LOUDER than it used to rather than quieter.
78
+ */
79
+ export declare function committedOf(node: ISymbioteNode): IMirror | undefined;
80
+ export declare function createElement(component: string, isText?: boolean, tag?: string): ISymbioteNode;
25
81
  export declare function createRawText(text: string): ISymbioteNode;
26
82
  export declare function isSymbioteNode(value: unknown): value is ISymbioteNode;
27
83
  export declare function debugNodeId(node: ISymbioteNode): number;
@@ -29,13 +85,53 @@ export declare const ANCHOR_COMPONENT = "#anchor";
29
85
  export declare function createAnchor(): ISymbioteNode;
30
86
  export declare function isAnchor(node: ISymbioteNode): boolean;
31
87
  export declare function isEmptyRawText(node: ISymbioteNode): boolean;
88
+ /**
89
+ * Change which Fabric view a node commits as, keeping the node's identity.
90
+ *
91
+ * The commit walk already re-creates a node whose `viewName` no longer matches its committed one —
92
+ * that is how a `<Text>` moving in or out of another `<Text>` flips between RCTText and
93
+ * RCTVirtualText (`commit.ts`, reason `view-kind`). This exposes the same door for a prop-driven
94
+ * view choice, so `intrinsicWhen` is honoured on UPDATE and not only at create.
95
+ *
96
+ * The POLICY stays out of the engine: which prop decides, and which view it decides between, lives
97
+ * in `HOST_PRIMITIVES` and is read by `resolveIntrinsicTag` in `@symbiote-native/components`. The
98
+ * engine only knows how to swap the name — the same split every other spec-driven fold has here.
99
+ *
100
+ * A no-op when the name is unchanged, so a renderer may call it on every update without comparing
101
+ * first.
102
+ */
103
+ export declare function setNodeComponent(node: ISymbioteNode, component: string): void;
32
104
  export declare function markDirty(node: ISymbioteNode): void;
105
+ export declare function markPropsDirty(node: ISymbioteNode): void;
106
+ export declare function markStructureDirty(parent: ISymbioteNode): void;
33
107
  export declare function takePropStats(): {
34
108
  writes: number;
35
109
  noops: number;
36
110
  };
37
111
  export declare function setProp(node: ISymbioteNode, key: string, value: unknown): void;
112
+ /**
113
+ * Install a listener the BEHAVIOR owns, bypassing the ownership check.
114
+ *
115
+ * `setEventListener` diverts an owned name into the stash, which is right for an app listener and
116
+ * circular for the behavior's own dispatcher — it would stash itself and never occupy the slot it
117
+ * exists to hold. This is the one writer allowed past that gate.
118
+ *
119
+ * `undefined` removes it, gate flag included. A behavior whose dispatcher is conditional needs
120
+ * that as much as it needs the install: ScrollView takes the owner's `layout` only while the app
121
+ * or an inverted sticky header wants it, and a one-way installer leaves `onLayout: true` standing
122
+ * in the payload of a ScrollView that no longer reads the event.
123
+ */
124
+ export declare function setBehaviorListener(node: ISymbioteNode, name: string, listener: IListener | undefined): void;
38
125
  export declare function setEventListener(node: ISymbioteNode, name: string, value: unknown): void;
126
+ export interface IClassStyleParts {
127
+ classStyle: unknown;
128
+ explicitStyle: unknown;
129
+ hiddenStyle: unknown;
130
+ className: IClassNameValue | undefined;
131
+ isPressed: boolean;
132
+ activeStyle: unknown;
133
+ activeStyleFromCallback: boolean;
134
+ }
39
135
  /**
40
136
  * Stop a node painting without unmounting it, or let it paint again.
41
137
  *
@@ -44,12 +140,26 @@ export declare function setEventListener(node: ISymbioteNode, name: string, valu
44
140
  * author's style byte for byte — belongs to whoever owns the style merge, and that is here.
45
141
  */
46
142
  export declare function setNodeHidden(node: ISymbioteNode, hidden: boolean): void;
143
+ /**
144
+ * Put a node into (or out of) its pressed state, so `.x:active` rules apply.
145
+ *
146
+ * The engine-owned half of what `:active` is on the web: the press state resolves BELOW the
147
+ * framework and never crosses into it, which is what lets a pressable be an intrinsic tag rather
148
+ * than a component (`.claude/rules/host-primitive-tier.md`, tier 2). A component is forced only
149
+ * when the TEMPLATE must read the state — `v-slot="{ pressed }"` and the function form of `style`
150
+ * — and this exists so the common case does not have to.
151
+ *
152
+ * Costs nothing when no `:active` rule is registered anywhere: `resolveActiveClassName` hands back
153
+ * the very same object the unpressed path returns, so `isAlreadyPublished` turns the re-push away
154
+ * and the node is never dirtied.
155
+ */
156
+ export declare function setNodePressed(node: ISymbioteNode, pressed: boolean): void;
47
157
  export declare function getExplicitStyle(node: ISymbioteNode): unknown;
48
158
  export declare function routeProp(node: ISymbioteNode, key: string, value: unknown): void;
49
159
  export declare function setText(node: ISymbioteNode, text: string): void;
50
- export declare function appendChild(parent: ISymbioteNode, child: ISymbioteNode): void;
51
- export declare function insertBefore(parent: ISymbioteNode, child: ISymbioteNode, beforeChild: ISymbioteNode): void;
52
- export declare function removeChild(parent: ISymbioteNode, child: ISymbioteNode): void;
160
+ export declare function appendChild(requestedParent: ISymbioteNode, child: ISymbioteNode): void;
161
+ export declare function insertBefore(requestedParent: ISymbioteNode, child: ISymbioteNode, beforeChild: ISymbioteNode | null): void;
162
+ export declare function removeChild(requestedParent: ISymbioteNode, child: ISymbioteNode): void;
53
163
  export interface ITreeCensus {
54
164
  nodes: number;
55
165
  anchors: number;