@symbiote-native/react 0.3.0 → 1.0.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 (44) hide show
  1. package/README.md +24 -9
  2. package/build/bootstrap.js +1 -1
  3. package/build/components/activity-indicator/shared.js +1 -1
  4. package/build/components/button.js +1 -1
  5. package/build/components/flat-list/index.js +5 -2
  6. package/build/components/input-accessory-view/index.js +6 -1
  7. package/build/components/keyboard-avoiding-view/index.js +32 -12
  8. package/build/components/modal/index.js +1 -1
  9. package/build/components/pressable/index.d.ts +2 -1
  10. package/build/components/pressable/index.js +17 -5
  11. package/build/components/safe-area-view/index.d.ts +1 -0
  12. package/build/components/scroll-view/index.android.js +16 -6
  13. package/build/components/scroll-view/index.ios.js +3 -1
  14. package/build/components/scroll-view/shared.js +56 -7
  15. package/build/components/scroll-view/sticky-header.d.ts +11 -2
  16. package/build/components/scroll-view/sticky-header.js +53 -11
  17. package/build/components/text-input/index.js +15 -2
  18. package/build/components/touchable/index.d.ts +2 -0
  19. package/build/components/touchable/index.js +131 -29
  20. package/build/components/touchable-native-feedback/index.js +1 -1
  21. package/build/components/virtualized-list/index.js +64 -24
  22. package/build/components/virtualized-section-list/index.d.ts +5 -0
  23. package/build/components/virtualized-section-list/index.js +24 -4
  24. package/build/components.d.ts +4 -4
  25. package/build/components.js +19 -17
  26. package/build/create-portal/index.js +1 -1
  27. package/build/create-tunnel/index.js +1 -1
  28. package/build/hooks/use-window-dimensions.js +1 -1
  29. package/build/host-config.d.ts +2 -2
  30. package/build/host-config.js +42 -11
  31. package/build/host-instance.js +1 -1
  32. package/build/index.d.ts +8 -7
  33. package/build/index.js +1 -1
  34. package/build/jsx.d.ts +18 -0
  35. package/build/jsx.js +1 -0
  36. package/build/modules/animated/create-animated-component.d.ts +2 -1
  37. package/build/modules/animated/create-animated-component.js +10 -2
  38. package/build/modules/animated/index.d.ts +1 -1
  39. package/build/modules/animated/index.js +1 -1
  40. package/build/modules/status-bar/index.ios.js +1 -1
  41. package/build/render.js +16 -8
  42. package/build/state-style.d.ts +1 -0
  43. package/build/state-style.js +13 -0
  44. package/package.json +24 -8
@@ -6,7 +6,7 @@
6
6
  // useImperativeHandle + the per-entry render dispatch). Lower layer in RN's
7
7
  // SectionList -> VirtualizedSectionList -> VirtualizedList stack.
8
8
  import { createElement, useImperativeHandle, useRef, } from 'react';
9
- import { dlog, Platform } from '@symbiote-native/engine';
9
+ import { dlog, Platform, } from '@symbiote-native/engine';
10
10
  import { flattenSections, resolveStickySectionHeaders, scrollLocationToFlatIndex, sectionEntryKey, unwrapEntryItem, } from '@symbiote-native/components';
11
11
  import { VirtualizedList, } from '../virtualized-list/index.js';
12
12
  function resolveSeparator(component) {
@@ -20,7 +20,10 @@ export function VirtualizedSectionList(props) {
20
20
  const { ref, sections, renderItem, renderSectionHeader, renderSectionFooter, SectionSeparatorComponent,
21
21
  // Pulled out of `rest`: the user's separator is typed on ItemT, but the inner VirtualizedList
22
22
  // streams ISectionEntry<ItemT>, so we wrap it to unwrap each entry back to its ItemT.
23
- ItemSeparatorComponent, keyExtractor, stickySectionHeadersEnabled, ...rest } = props;
23
+ ItemSeparatorComponent, keyExtractor,
24
+ // Also pulled out of `rest`: the inner list streams entries, the user's callback expects
25
+ // sections. See entryItemLayout below.
26
+ getItemLayout, stickySectionHeadersEnabled, ...rest } = props;
24
27
  const { entries, headerIndices } = flattenSections(sections, SectionSeparatorComponent !== undefined);
25
28
  // RN sticks section headers by default only on iOS; Android does not unless asked. headerIndices
26
29
  // are the flat positions of every section header; VirtualizedList forwards the in-window ones.
@@ -58,10 +61,14 @@ export function VirtualizedSectionList(props) {
58
61
  const renderEntry = (info) => {
59
62
  const entry = info.item;
60
63
  if (entry.kind === 'header') {
61
- return renderSectionHeader ? renderSectionHeader({ section: entry.section }) : undefined;
64
+ return renderSectionHeader
65
+ ? renderSectionHeader({ section: entry.section })
66
+ : undefined;
62
67
  }
63
68
  if (entry.kind === 'footer') {
64
- return renderSectionFooter ? renderSectionFooter({ section: entry.section }) : undefined;
69
+ return renderSectionFooter
70
+ ? renderSectionFooter({ section: entry.section })
71
+ : undefined;
65
72
  }
66
73
  if (entry.kind === 'section-separator') {
67
74
  return resolveSeparator(SectionSeparatorComponent);
@@ -83,6 +90,18 @@ export function VirtualizedSectionList(props) {
83
90
  trailingItem: unwrapEntryItem(entryProps.trailingItem),
84
91
  });
85
92
  const entryKeyExtractor = (entry, index) => sectionEntryKey(entry, index, keyExtractor);
93
+ // Hand the callback `sections`, not the entries: RN's inner VirtualizedList gets
94
+ // `data={this.props.sections}` (VirtualizedSectionList.js:216) while ours streams the FLATTENED
95
+ // entries, so the same user code would otherwise see a different argument here than on RN.
96
+ //
97
+ // UPSTREAM-DIVERGENCE(react-native): the flat INDEX matches RN's (two rows per section, header
98
+ // and footer) only while SectionSeparatorComponent is unset. With it, flattenSections emits an
99
+ // extra 'section-separator' row per boundary that RN renders inside the neighbouring cell, so
100
+ // indices shift by one per boundary from the second section on. Deliberate - that row is how this
101
+ // adapter paints the separator; a caller combining the two must account for it.
102
+ const entryItemLayout = getItemLayout === undefined
103
+ ? undefined
104
+ : (_entries, index) => getItemLayout(sections, index);
86
105
  return createElement((VirtualizedList), {
87
106
  ref: listRef,
88
107
  data: entries,
@@ -90,6 +109,7 @@ export function VirtualizedSectionList(props) {
90
109
  getItemCount: () => entries.length,
91
110
  renderItem: renderEntry,
92
111
  keyExtractor: entryKeyExtractor,
112
+ getItemLayout: entryItemLayout,
93
113
  stickyHeaderIndices,
94
114
  ItemSeparatorComponent: entrySeparatorComponent,
95
115
  ...rest,
@@ -1,7 +1,7 @@
1
- import { type FC, type Ref, type ReactNode } from 'react';
1
+ import type { Ref, ReactNode } from 'react';
2
2
  import type { ISymbioteEvent } from '@symbiote-native/engine';
3
3
  import type { IHostInstance } from './host-instance';
4
- import { type IAccessibilityProps, type IAriaProps } from '@symbiote-native/components';
4
+ import type { IAccessibilityProps, IAriaProps } from '@symbiote-native/components';
5
5
  import type { IResponderProps } from './utils/responder-props';
6
6
  import type { IStyleProp, ITextStyle, IViewStyle } from './utils/styles';
7
7
  export interface IViewProps extends IAccessibilityProps, IAriaProps, IResponderProps {
@@ -50,5 +50,5 @@ export interface ITextProps extends IAccessibilityProps, IAriaProps {
50
50
  ref?: Ref<IHostInstance>;
51
51
  children?: ReactNode;
52
52
  }
53
- export declare const View: FC<IViewProps>;
54
- export declare const Text: FC<ITextProps>;
53
+ export declare const View = "symbiote-view";
54
+ export declare const Text = "symbiote-text";
@@ -1,17 +1,19 @@
1
- // Host primitives exposed to user code. They are thin wrappers that produce the
2
- // lowercase host elements the reconciler understands (`view` / `text`); the
3
- // reconciler maps those to shared's mutation API, which resolves them to Fabric
4
- // view names at commit.
5
- import { createElement } from 'react';
6
- import { resolveAccessibilityProps, } from '@symbiote-native/components';
7
- // RN's modern `id` is just a W3C-named alias for `nativeID`: View.js copies it over
8
- // (`processedProps.nativeID = id`), so `id` wins when both are set. We fold it here and
9
- // blank the alias so a raw `id` never reaches Fabric (every non-function prop passes
10
- // through to the slot otherwise).
11
- function resolveId({ id, ...rest }) {
12
- if (id === undefined)
13
- return rest;
14
- return { ...rest, nativeID: id };
15
- }
16
- export const View = props => createElement('symbiote-view', resolveAccessibilityProps(resolveId(props)));
17
- export const Text = props => createElement('symbiote-text', resolveAccessibilityProps(props));
1
+ // Host primitives exposed to user code. They ARE the intrinsic tags — a capitalized export whose
2
+ // value is the tag string — so an app writes `<View>` and React commits a host element directly,
3
+ // with no component instance in between. The reconciler maps the tag through
4
+ // `descriptorFor` to a Fabric view name at commit.
5
+ // `View` and `Text` are the intrinsic TAGS, not components wrapping them. JSX resolves a
6
+ // capitalized tag to the value in scope, and a value that is a STRING is a host element to React —
7
+ // so `<View/>` compiles to `_jsx('symbiote-view', …)` with no component instance, while
8
+ // `JSX.IntrinsicElements['symbiote-view']` still supplies the strict props (a bad prop is TS2322).
9
+ //
10
+ // The two folds their component bodies used to apply both moved down a layer, which is what let
11
+ // the bodies go:
12
+ //
13
+ // aria / accessibility the engine folds every node in `fabricProps` (accessibility-props.ts)
14
+ // id -> nativeID, the renderer folds by spec in `host-config`'s createInstance
15
+ // Text's defaults (`foldHostBag`, driven by HOST_PRIMITIVES)
16
+ //
17
+ // A type annotation rather than `as const`: same literal type, no cast (`ts-js-best-practices`).
18
+ export const View = 'symbiote-view';
19
+ export const Text = 'symbiote-text';
@@ -10,7 +10,7 @@
10
10
  // call site (e.g. a ref to a persistent "overlay host" View near your app root) — not a second,
11
11
  // independently-mounted SymbioteSurface. Cross-surface targets aren't wired: resetAfterCommit only
12
12
  // fires for the primary root's own container, so a different surface's tree would never repaint.
13
- import { isSymbioteNode, SymbioteSurface } from '@symbiote-native/engine';
13
+ import { isSymbioteNode, SymbioteSurface, } from '@symbiote-native/engine';
14
14
  import reconciler from '../host-config.js';
15
15
  export function createPortal(children, container, key) {
16
16
  if (!(container instanceof SymbioteSurface) && !isSymbioteNode(container)) {
@@ -15,7 +15,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
15
15
  // nowhere to bounce back to. `Out` lives in whichever surface should PAINT the content; `In`
16
16
  // lives anywhere - same surface or a different one - and never touches a Fabric node
17
17
  // directly, so there's no "target must already be mounted" guard to satisfy.
18
- import { Fragment, useEffect, useId, useSyncExternalStore } from 'react';
18
+ import { Fragment, useEffect, useId, useSyncExternalStore, } from 'react';
19
19
  export function createTunnel() {
20
20
  const items = new Map();
21
21
  const listeners = new Set();
@@ -4,7 +4,7 @@
4
4
  // subscribing to close the gap between the render-time get and the effect-time
5
5
  // listener. Only the window metrics changing triggers a re-render.
6
6
  import { useEffect, useState } from 'react';
7
- import { Dimensions } from '@symbiote-native/engine';
7
+ import { Dimensions, } from '@symbiote-native/engine';
8
8
  export function useWindowDimensions() {
9
9
  const [dimensions, setDimensions] = useState(() => Dimensions.get('window'));
10
10
  useEffect(() => {
@@ -1,7 +1,7 @@
1
1
  import createReconciler from 'react-reconciler';
2
2
  import { SymbioteSurface, type ISymbioteNode } from '@symbiote-native/engine';
3
- import { type IHostInstance } from './host-instance';
4
3
  type IContainer = SymbioteSurface | ISymbioteNode;
5
4
  export declare function withDiscretePriority(run: () => void): void;
6
- declare const reconciler: createReconciler.Reconciler<IContainer, ISymbioteNode, ISymbioteNode, never, unknown, IHostInstance>;
5
+ declare const reconciler: createReconciler.Reconciler<IContainer, ISymbioteNode, ISymbioteNode, never, unknown, ISymbioteNode>;
6
+ export declare function flushExternalUpdate(run: () => void): void;
7
7
  export default reconciler;
@@ -4,7 +4,7 @@
4
4
  // canary: a known-good driver exercising shared end to end.
5
5
  import createReconciler from 'react-reconciler';
6
6
  import { createContext } from 'react';
7
- import { appendChild, createElement, createRawText, insertBefore, removeChild, routeProp, setText, SymbioteSurface, } from '@symbiote-native/engine';
7
+ import { appendChild, createElement, createRawText, insertBefore, removeChild, routeProp, setNodeComponent, setNodeHidden, setText, SymbioteSurface, } from '@symbiote-native/engine';
8
8
  import { DefaultEventPriority, DiscreteEventPriority, NoEventPriority, } from './reconciler-constants.js';
9
9
  import { toPublicInstance } from './host-instance.js';
10
10
  // Intrinsic JSX type -> Fabric component name. The name table + resolver live once in
@@ -13,6 +13,14 @@ import { toPublicInstance } from './host-instance.js';
13
13
  // Adding a primitive is one entry in each name table there, plus its thin
14
14
  // component in components.ts: no host-config logic per primitive.
15
15
  import { descriptorFor } from '@symbiote-native/components';
16
+ // A bare intrinsic tag has no wrapper to apply RN's per-primitive prop folds (id -> nativeID,
17
+ // Text's ellipsizeMode / allowFontScaling defaults), so the renderer is the layer that must.
18
+ // Shared with every other adapter, driven by the same HOST_PRIMITIVES spec.
19
+ import { foldHostBag } from '@symbiote-native/components/fold-host-bag';
20
+ // WHICH native view a primitive commits can depend on a prop (TextInput's `multiline`). A lowering
21
+ // transform decides that from source text; a bare tag has no transform, so the choice is made here,
22
+ // where the runtime value is known. Identity for every primitive that declares no alternative.
23
+ import { resolveIntrinsicTag } from '@symbiote-native/components/resolve-intrinsic';
16
24
  // React-reserved prop keys that must never reach Fabric. `children` is the element
17
25
  // tree (handled via appendChild). `ref`/`key` are React 19 plain props: the reconciler
18
26
  // reads `props.ref` for commitAttachRef but does NOT strip it from the props it hands a
@@ -47,8 +55,9 @@ function isSurfaceContainer(container) {
47
55
  return container instanceof SymbioteSurface;
48
56
  }
49
57
  let currentUpdatePriority = NoEventPriority;
50
- // Run an externally-triggered update (a native event) at discrete priority so it
51
- // lands on the sync lane and flushSyncWork paints it immediately.
58
+ // Run the callback at discrete priority. Native events and component-owned timers both enter
59
+ // React outside its render loop; callers pair this with flushExternalUpdate when the result must
60
+ // paint synchronously.
52
61
  export function withDiscretePriority(run) {
53
62
  const previous = currentUpdatePriority;
54
63
  currentUpdatePriority = DiscreteEventPriority;
@@ -70,7 +79,9 @@ const reconciler = createReconciler({
70
79
  getRootHostContext: () => ({ isInsideText: false }),
71
80
  getChildHostContext(parentHostContext, type) {
72
81
  const isInsideText = descriptorFor(type).isText;
73
- return parentHostContext.isInsideText === isInsideText ? parentHostContext : { isInsideText };
82
+ return parentHostContext.isInsideText === isInsideText
83
+ ? parentHostContext
84
+ : { isInsideText };
74
85
  },
75
86
  getPublicInstance: instance => toPublicInstance(instance),
76
87
  prepareForCommit: () => null,
@@ -88,12 +99,12 @@ const reconciler = createReconciler({
88
99
  },
89
100
  shouldSetTextContent: () => false,
90
101
  createInstance(type, props, _container, hostContext) {
91
- const descriptor = descriptorFor(type);
102
+ const descriptor = descriptorFor(resolveIntrinsicTag(type, props));
92
103
  if (hostContext.isInsideText && !descriptor.isText) {
93
104
  throw new Error(`<${type}> can't be nested inside <Text>`);
94
105
  }
95
106
  const node = createElement(descriptor.component, descriptor.isText);
96
- applyProps(node, props);
107
+ applyProps(node, foldHostBag(type, props));
97
108
  return node;
98
109
  },
99
110
  createTextInstance(text, _container, hostContext) {
@@ -125,8 +136,14 @@ const reconciler = createReconciler({
125
136
  removeChild(container, child);
126
137
  },
127
138
  finalizeInitialChildren: () => false,
128
- commitUpdate(node, _type, oldProps, newProps) {
129
- applyUpdate(node, oldProps, newProps);
139
+ commitUpdate(node, type, oldProps, newProps) {
140
+ // A primitive whose native view depends on a prop (TextInput's `multiline`) can change view on
141
+ // an update. The node keeps its identity — an app's ref stays live — and the commit walk
142
+ // re-creates the Fabric node because its viewName no longer matches the committed one.
143
+ setNodeComponent(node, descriptorFor(resolveIntrinsicTag(type, newProps)).component);
144
+ // BOTH sides folded, or the diff compares a raw `id` against a folded `nativeID` and writes
145
+ // the alias twice while never clearing the raw key.
146
+ applyUpdate(node, foldHostBag(type, oldProps), foldHostBag(type, newProps));
130
147
  },
131
148
  commitTextUpdate(node, _oldText, newText) {
132
149
  setText(node, newText);
@@ -134,8 +151,12 @@ const reconciler = createReconciler({
134
151
  resetTextContent: () => { },
135
152
  hideTextInstance: node => setText(node, ''),
136
153
  unhideTextInstance: (node, text) => setText(node, text),
137
- hideInstance: () => { },
138
- unhideInstance: () => { },
154
+ // `Activity mode="hidden"` and a re-suspending `Suspense` both reach the tree through these.
155
+ // They were no-ops, so hidden content went on painting — stacked on top of whatever replaced
156
+ // it — and only the mount/unmount half of Activity appeared to work. The engine owns the
157
+ // reversible half (see setNodeHidden): the author's own style stays untouched underneath.
158
+ hideInstance: node => setNodeHidden(node, true),
159
+ unhideInstance: node => setNodeHidden(node, false),
139
160
  beforeActiveInstanceBlur: () => { },
140
161
  afterActiveInstanceBlur: () => { },
141
162
  detachDeletedInstance: () => { },
@@ -146,7 +167,9 @@ const reconciler = createReconciler({
146
167
  currentUpdatePriority = priority;
147
168
  },
148
169
  getCurrentUpdatePriority: () => currentUpdatePriority,
149
- resolveUpdatePriority: () => currentUpdatePriority !== NoEventPriority ? currentUpdatePriority : DefaultEventPriority,
170
+ resolveUpdatePriority: () => currentUpdatePriority !== NoEventPriority
171
+ ? currentUpdatePriority
172
+ : DefaultEventPriority,
150
173
  maySuspendCommit: () => false,
151
174
  NotPendingTransition: null,
152
175
  // react's Context IS the runtime value react-reconciler's ReactContext wants
@@ -166,4 +189,12 @@ const reconciler = createReconciler({
166
189
  suspendInstance: () => { },
167
190
  waitForCommitToBeReady: () => null,
168
191
  });
192
+ // Native-event and timer callbacks both update React from outside the reconciler. Put the update
193
+ // on the discrete lane and synchronously commit it; otherwise a delayed Pressable transition can
194
+ // mutate state but remain visually stale until some unrelated later event flushes React work.
195
+ export function flushExternalUpdate(run) {
196
+ withDiscretePriority(run);
197
+ // @ts-expect-error flushSyncWork exists at runtime in react-reconciler 0.33
198
+ reconciler.flushSyncWork();
199
+ }
169
200
  export default reconciler;
@@ -1,7 +1,7 @@
1
1
  // The public instance a host ref hands back, and the ref -> tag lookup. toPublicInstance and
2
2
  // IHostInstance live in @symbiote-native/engine (every adapter inherits the same public instance);
3
3
  // this module re-exports them so the React surface (findNodeHandle + IHostInstance, host-config's
4
- // toPublicInstance graft) is unchanged. findNodeHandle stays React-specific: it is the
4
+ // getPublicInstance) is unchanged. findNodeHandle stays React-specific: it is the
5
5
  // RN-shaped "ref -> native tag" lookup over the engine's getNativeTag.
6
6
  import { getNativeTag, isSymbioteNode, toPublicInstance, } from '@symbiote-native/engine';
7
7
  export { toPublicInstance };
package/build/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export type { ISymbioteIntrinsicTag } from './jsx';
1
2
  export { View, Text } from './components';
2
3
  export type { IViewProps, ITextProps } from './components';
3
4
  export type { IAccessibilityProps, IAriaProps, IAccessibilityRole, IRole, IAccessibilityStateValue, IAccessibilityValue, IAccessibilityActionInfo, } from '@symbiote-native/components';
@@ -7,9 +8,9 @@ export type { IImageProps, IImageSource, IImageSourceProp, IResizeMode, IImageSi
7
8
  export { ImageBackground } from './components/image-background';
8
9
  export type { IImageBackgroundProps } from './components/image-background';
9
10
  export { ScrollView } from './components/scroll-view';
10
- export type { IScrollViewProps, IScrollViewHandle } from './components/scroll-view';
11
+ export type { IScrollViewProps, IScrollViewHandle, } from './components/scroll-view';
11
12
  export { TextInput } from './components/text-input';
12
- export type { ITextInputProps, ITextInputHandle } from './components/text-input';
13
+ export type { ITextInputProps, ITextInputHandle, } from './components/text-input';
13
14
  export { InputAccessoryView } from './components/input-accessory-view';
14
15
  export type { IInputAccessoryViewProps } from './components/input-accessory-view';
15
16
  export { KeyboardAvoidingView } from './components/keyboard-avoiding-view';
@@ -35,12 +36,12 @@ export type { IButtonProps } from './components/button';
35
36
  export { FlatList } from './components/flat-list';
36
37
  export type { IFlatListProps, IFlatListHandle } from './components/flat-list';
37
38
  export { SectionList } from './components/section-list';
38
- export type { ISectionListProps, ISectionListHandle, ISection } from './components/section-list';
39
+ export type { ISectionListProps, ISectionListHandle, ISection, } from './components/section-list';
39
40
  export { VirtualizedSectionList } from './components/virtualized-section-list';
40
41
  export type { IVirtualizedSectionListProps, IVirtualizedSectionListHandle, } from './components/virtualized-section-list';
41
42
  export { VirtualizedList } from './components/virtualized-list';
42
43
  export type { IVirtualizedListProps, IVirtualizedListHandle, IViewToken, IViewableItemsChangedInfo, IViewabilityConfig, IViewabilityConfigCallbackPair, } from './components/virtualized-list';
43
- export type { IViewStyle, ITextStyle, IFlexAlign, IFlexJustify } from './utils/styles';
44
+ export type { IViewStyle, ITextStyle, IFlexAlign, IFlexJustify, } from './utils/styles';
44
45
  export { mount, unmount } from './render';
45
46
  export { createPortal, type IPortalContainer } from './create-portal';
46
47
  export { createTunnel, type ITunnel } from './create-tunnel';
@@ -51,11 +52,11 @@ export { AppRegistry, setHostRegistrar } from './modules/app-registry';
51
52
  export type { IComponentProvider, IAppParameters, IRunnable, IHostRegistrar, IWrapperComponentProvider, IRegistry, IHeadlessTask, ITaskProvider, ITaskCanceller, ITaskCancelProvider, } from './modules/app-registry';
52
53
  export { Animated, createAnimatedComponent } from './modules/animated';
53
54
  export { Platform, StyleSheet } from '@symbiote-native/engine';
54
- export { PlatformColor, DynamicColorIOS, processColor } from '@symbiote-native/engine';
55
+ export { PlatformColor, DynamicColorIOS, processColor, } from '@symbiote-native/engine';
55
56
  export type { IColorValue, IOpaqueColorValue, IDynamicColorIOSTuple, } from '@symbiote-native/engine';
56
57
  export { setNativeViewConfigSource, setColorProcessor, setDeviceEventSource, } from '@symbiote-native/engine';
57
58
  export { dlog, isDebug } from '@symbiote-native/engine';
58
- export type { INativeViewConfig, INativeViewConfigSource } from '@symbiote-native/engine';
59
+ export type { INativeViewConfig, INativeViewConfigSource, } from '@symbiote-native/engine';
59
60
  export type { IPlatformStatic, IPlatformOSType, IPlatformConstantsIOS, IPlatformConstantsAndroid, IPlatformSelectSpec, } from '@symbiote-native/engine';
60
61
  export { Alert, Share, ActionSheetIOS, Linking, Vibration, ToastAndroid, Settings, I18nManager, Dimensions, Appearance, AppState, Keyboard, KEYBOARD_EVENT, BackHandler, PermissionsAndroid, PERMISSIONS, RESULTS, LayoutAnimation, PixelRatio, } from '@symbiote-native/engine';
61
62
  export type { IAlertType, IAlertButtonStyle, IAlertButton, IAlertButtons, IAlertOptions, IShareContent, IShareOptions, IShareAction, IActionSheetIOSOptions, IShareActionSheetIOSOptions, IShareActionSheetError, IUrlEvent, II18nManagerConstants, IDisplayMetrics, IDisplayMetricsAndroid, IDimensionsPayload, IDimensionsSet, IDimensionsKey, IDimensionsChangeListener, IDimensionsStatic, IColorSchemeName, IColorSchemePreference, IAppStateStatus, IAppStateEvent, IKeyboardEventName, IKeyboardEvent, IKeyboardMetrics, IBackPressEventName, IBackPressHandler, IPermission, IPermissionStatus, IRationale, ILayoutAnimationType, ILayoutAnimationProperty, ILayoutAnimationConfig, ILayoutAnimationAnim, IPixelRatioStatic, } from '@symbiote-native/engine';
@@ -69,5 +70,5 @@ export { InteractionManager } from '@symbiote-native/engine';
69
70
  export type { IInteractionEvent, ISimpleTask, IPromiseTask, ITask, IHandle, } from '@symbiote-native/engine';
70
71
  export { TouchableNativeFeedback } from './components/touchable-native-feedback';
71
72
  export type { ITouchableNativeFeedbackProps, INativeFeedbackBackground, IThemeAttrBackground, IRippleBackground, } from './components/touchable-native-feedback';
72
- export type { ISymbioteEvent, ISymbioteNode, IRootTag } from '@symbiote-native/engine';
73
+ export type { ISymbioteEvent, ISymbioteNode, IRootTag, } from '@symbiote-native/engine';
73
74
  export type { ICellLayout, ISeparatorProps, ISeparators, IModalOrientationChangeEvent, IPressableAndroidRippleConfig, IEnterKeyHint, IInputMode, ISubmitBehavior, ITextInputSelection, IImageStatics, } from '@symbiote-native/components';
package/build/index.js CHANGED
@@ -49,7 +49,7 @@ export { Platform, StyleSheet } from '@symbiote-native/engine';
49
49
  // Color utilities: PlatformColor / DynamicColorIOS build opaque platform colors;
50
50
  // processColor runs a color through the injected platform processor. All pure /
51
51
  // seam-backed, so they live in shared and the adapter re-exports them.
52
- export { PlatformColor, DynamicColorIOS, processColor } from '@symbiote-native/engine';
52
+ export { PlatformColor, DynamicColorIOS, processColor, } from '@symbiote-native/engine';
53
53
  // The three app-entry seams, wired once on a real host, so the barrel exposes them together.
54
54
  // setNativeViewConfigSource hands the engine RN's ViewConfig registry, which is how third-party
55
55
  // Fabric views auto-derive their metadata:
package/build/jsx.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import type { ISymbioteIntrinsic } from '@symbiote-native/components';
2
+ import type { IViewProps, ITextProps } from './components';
3
+ interface HostProps {
4
+ style?: unknown;
5
+ children?: import('react').ReactNode;
6
+ [key: string]: unknown;
7
+ }
8
+ type ILooseIntrinsics = Omit<Record<ISymbioteIntrinsic, HostProps>, 'symbiote-view' | 'symbiote-text'>;
9
+ declare module 'react' {
10
+ namespace JSX {
11
+ interface IntrinsicElements extends ILooseIntrinsics {
12
+ 'symbiote-view': IViewProps;
13
+ 'symbiote-text': ITextProps;
14
+ }
15
+ }
16
+ }
17
+ export type ISymbioteIntrinsicTag = keyof import('react').JSX.IntrinsicElements;
18
+ export {};
package/build/jsx.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -8,5 +8,6 @@ type IAnimatableProps = {
8
8
  style?: unknown;
9
9
  children?: unknown;
10
10
  };
11
- export declare function createAnimatedComponent<P extends IAnimatableProps>(Component: ComponentType<P>): ComponentType<IAnimatedComponentProps>;
11
+ type IAnimatableBase<P extends IAnimatableProps> = ComponentType<P> | string;
12
+ export declare function createAnimatedComponent<P extends IAnimatableProps>(Component: IAnimatableBase<P>): ComponentType<IAnimatedComponentProps>;
12
13
  export {};
@@ -27,6 +27,12 @@ function assignRef(ref, value) {
27
27
  }
28
28
  ref.current = value;
29
29
  }
30
+ // A tag has no displayName/name, so read the label through the shape actually present.
31
+ function baseNameOf(base) {
32
+ if (typeof base === 'string')
33
+ return base;
34
+ return base.displayName ?? base.name ?? 'Anonymous';
35
+ }
30
36
  export function createAnimatedComponent(Component) {
31
37
  function AnimatedComponent(props) {
32
38
  const { ref: forwardedRef, passthroughAnimatedPropExplicitValues: passthrough, ...rest } = props;
@@ -74,11 +80,13 @@ export function createAnimatedComponent(Component) {
74
80
  const passthroughStyle = readPassthroughStyle(passthrough);
75
81
  if (passthroughStyle !== undefined) {
76
82
  reduced.style =
77
- reduced.style === undefined ? passthroughStyle : [reduced.style, passthroughStyle];
83
+ reduced.style === undefined
84
+ ? passthroughStyle
85
+ : [reduced.style, passthroughStyle];
78
86
  }
79
87
  const childProps = Object.assign(Object.create(null), reduced, { ref: captureRef });
80
88
  return createElement(Component, childProps);
81
89
  }
82
- AnimatedComponent.displayName = `Animated(${Component.displayName ?? Component.name ?? 'Anonymous'})`;
90
+ AnimatedComponent.displayName = `Animated(${baseNameOf(Component)})`;
83
91
  return AnimatedComponent;
84
92
  }
@@ -1,7 +1,7 @@
1
1
  import { AnimatedValue, AnimatedValueXY, AnimatedColor, timing, spring, decay, parallel, sequence, stagger, loop, delay, add, subtract, multiply, divide, modulo, diffClamp, event, forkEvent, unforkEvent } from '@symbiote-native/engine';
2
2
  import { createAnimatedComponent } from './create-animated-component';
3
3
  export { createAnimatedComponent } from './create-animated-component';
4
- export { AnimatedProps, AnimatedStyle, AnimatedTransform } from '@symbiote-native/engine';
4
+ export { AnimatedProps, AnimatedStyle, AnimatedTransform, } from '@symbiote-native/engine';
5
5
  export declare const Animated: {
6
6
  Value: typeof AnimatedValue;
7
7
  ValueXY: typeof AnimatedValueXY;
@@ -14,7 +14,7 @@ import { AnimatedMock } from '@symbiote-native/engine';
14
14
  export { createAnimatedComponent } from './create-animated-component.js';
15
15
  // The pure graph leaves now live in @symbiote-native/engine (they extend AnimatedWithChildren,
16
16
  // no React); re-exported here so @symbiote-native/react's Animated surface is unchanged.
17
- export { AnimatedProps, AnimatedStyle, AnimatedTransform } from '@symbiote-native/engine';
17
+ export { AnimatedProps, AnimatedStyle, AnimatedTransform, } from '@symbiote-native/engine';
18
18
  // View/Text/Image are pure host primitives, so wrap them eagerly.
19
19
  const AnimatedView = createAnimatedComponent(View);
20
20
  const AnimatedText = createAnimatedComponent(Text);
@@ -10,7 +10,7 @@
10
10
  // it, which is also what a bare '@symbiote-native/engine' import resolves to under vitest — so
11
11
  // this file's own engine import is already vitest-correct, no platform-selection subtlety here.
12
12
  import { useEffect } from 'react';
13
- import { applyStatusBarProps, statusBarImperative } from '@symbiote-native/engine';
13
+ import { applyStatusBarProps, statusBarImperative, } from '@symbiote-native/engine';
14
14
  // StatusBar renders null and applies its props to the native module in an effect,
15
15
  // on mount and on every prop change. Simplification vs RN: RN maintains a
16
16
  // prop-merge stack so nested StatusBars compose (deepest/last wins). This module
package/build/render.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // rootTag (via AppRegistry.registerRunnable); we create a surface for it and let
3
3
  // the reconciler drive shared, which commits into nativeFabricUIManager.
4
4
  import { createSurface, disposeRoot, setEventDispatcher, dlog, reportUncaughtError, } from '@symbiote-native/engine';
5
- import reconciler, { withDiscretePriority } from './host-config.js';
5
+ import reconciler, { flushExternalUpdate } from './host-config.js';
6
6
  import { LegacyRoot } from './reconciler-constants.js';
7
7
  const noop = () => { };
8
8
  function errorReporter(origin) {
@@ -11,10 +11,20 @@ function errorReporter(origin) {
11
11
  };
12
12
  }
13
13
  const onUncaughtError = errorReporter('react render (no error boundary)');
14
- // A boundary handled it, and it is still reported - the boundary decides what the USER sees, not
15
- // whether the developer hears about it. Upstream's nativeOnCaughtError calls the same
16
- // showErrorDialog as the uncaught path.
17
- const onCaughtError = errorReporter('react render (caught by error boundary)');
14
+ // A boundary handled it, so it does NOT reach the native redbox — it goes to , off unless
15
+ // DEBUG is set.
16
+ //
17
+ // This deliberately DIVERGES from upstream, whose nativeOnCaughtError calls the same
18
+ // showErrorDialog as the uncaught path. The argument for upstream's choice is that the boundary
19
+ // decides what the USER sees, not whether the developer hears. The argument against, and the one
20
+ // this project takes: writing an ErrorBoundary IS the developer saying "I know this can throw and
21
+ // I am handling it here". Answering that with a full-screen redbox over the fallback the app just
22
+ // rendered contradicts the thing the app asked for, and it is the only adapter here that did it —
23
+ // Solid's ErrorBoundary is silent, and its canary reads as correct next to React's alarming one.
24
+ // An UNCAUGHT error still hits the redbox; the difference is exactly whether someone claimed it.
25
+ const onCaughtError = (error, info) => {
26
+ dlog(() => `react render (caught by error boundary): ${String(error)}${info.componentStack ?? ''}`);
27
+ };
18
28
  // React recovered on its own (a concurrent render retried and succeeded), so this is not fatal -
19
29
  // but it is still a real error the app swallowed a first render over. RN keeps React's
20
30
  // defaultOnRecoverableError here, which reports it globally.
@@ -30,10 +40,8 @@ const onRecoverableError = errorReporter('react render (recovered)');
30
40
  // to compare against Vue/Angular's microtask-coalesced requestCommit(). Kept
31
41
  // behind DEBUG per <keep_logs_gate_behind_DEBUG>, never removed.
32
42
  setEventDispatcher(run => {
33
- withDiscretePriority(run);
34
43
  dlog('react event-dispatch: forced flushSyncWork');
35
- // @ts-expect-error flushSyncWork exists at runtime in react-reconciler 0.33
36
- reconciler.flushSyncWork();
44
+ flushExternalUpdate(run);
37
45
  });
38
46
  const containers = new Map();
39
47
  // Unmount a surface's React tree (render null → empty completeRoot, clearing the
@@ -0,0 +1 @@
1
+ export { resolveStateStyle, type IPressStateArgument, type IResolvedStateStyle, } from '@symbiote-native/components/state-style';
@@ -0,0 +1,13 @@
1
+ // Re-export only. The implementation is shared (`@symbiote-native/components`) because two adapters
2
+ // had written byte-identical copies; this file exists so the code a transform EMITS keeps importing
3
+ // from the adapter package the app already depends on, rather than reaching past it.
4
+ //
5
+ // Imported from the SUBPATH, not the barrel: every adapter barrel re-exports
6
+ // `@symbiote-native/components` wholesale, so a name on that barrel silently becomes public API on
7
+ // all five — which is how a transform internal nearly shipped as a supported export.
8
+ //
9
+ // Present on every adapter, including those with no lowering transform yet: a per-framework subpath
10
+ // that four of five declare is exactly the silent gap `tests/package-subpath-parity.test.ts` exists
11
+ // to catch, and the file costs a re-export. Deliberately NOT on the public barrel — the emitted
12
+ // code names it, app code does not.
13
+ export { resolveStateStyle, } from '@symbiote-native/components/state-style';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/react",
3
- "version": "0.3.0",
3
+ "version": "1.0.0",
4
4
  "description": "SymbioteNative's React adapter — a react-reconciler host config (mutation mode) driving real native iOS/Android views through stock React Native Fabric.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -13,6 +13,18 @@
13
13
  "url": "https://github.com/OneEyed1366/symbiote-native/issues"
14
14
  },
15
15
  "author": "Andrey Prokopenko <psevdoproger@gmail.com>",
16
+ "keywords": [
17
+ "react-native",
18
+ "symbiote-native",
19
+ "react",
20
+ "fabric",
21
+ "reconciler",
22
+ "renderer",
23
+ "cross-platform",
24
+ "mobile",
25
+ "ios",
26
+ "android"
27
+ ],
16
28
  "type": "module",
17
29
  "main": "./build/index.js",
18
30
  "module": "./build/index.js",
@@ -26,7 +38,11 @@
26
38
  "types": "./build/bootstrap.d.ts",
27
39
  "default": "./build/bootstrap.js"
28
40
  },
29
- "./metro-css-parser": "./metro-css-parser.cjs"
41
+ "./metro-css-parser": "./metro-css-parser.cjs",
42
+ "./state-style": {
43
+ "types": "./build/state-style.d.ts",
44
+ "default": "./build/state-style.js"
45
+ }
30
46
  },
31
47
  "files": [
32
48
  "build",
@@ -37,20 +53,20 @@
37
53
  },
38
54
  "dependencies": {
39
55
  "react-reconciler": "^0.33.0",
40
- "@symbiote-native/components": "0.4.0",
41
- "@symbiote-native/css-parser": "0.3.0"
56
+ "@symbiote-native/components": "1.0.0",
57
+ "@symbiote-native/css-parser": "0.5.0"
42
58
  },
43
59
  "peerDependencies": {
44
- "@symbiote-native/engine": ">=0.1.7",
45
60
  "react": ">=19.0.0",
46
- "react-native": ">=0.86"
61
+ "react-native": ">=0.86",
62
+ "@symbiote-native/engine": "^0.4.0"
47
63
  },
48
64
  "devDependencies": {
49
65
  "@types/react": "^19.2.0",
50
66
  "@types/react-reconciler": "^0.32.2",
51
67
  "react": "19.2.3",
52
- "@symbiote-native/engine": "0.2.0",
53
- "@symbiote-native/test-utils": "0.1.6"
68
+ "@symbiote-native/test-utils": "0.3.0",
69
+ "@symbiote-native/engine": "0.4.0"
54
70
  },
55
71
  "scripts": {
56
72
  "typecheck": "tsc --build",