@symbiote-native/components 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # @symbiote-native/components
2
2
 
3
- The **framework-agnostic component layer** of [SymbioteJS](../../README.md) — pure state machines
3
+ The **framework-agnostic component layer** of [SymbioteNative](../../README.md) — pure state machines
4
4
  and pure render functions for every visual component (`Switch`, `Modal`, `ScrollView`, the
5
5
  `FlatList`/`SectionList` family, …), written **once** and inherited by every framework adapter
6
6
  (`@symbiote-native/react`, `@symbiote-native/vue`, `@symbiote-native/angular`, and the ones after them). It
7
7
  exists so that "add component X to a new adapter" means writing a thin lifecycle + descriptor
8
8
  bridge, not re-implementing X's logic per framework.
9
9
 
10
- > New to SymbioteJS? The [root README](../../README.md) has the architecture. This package is
10
+ > New to SymbioteNative? The [root README](../../README.md) has the architecture. This package is
11
11
  > "Workstream B" — the piece that makes cross-adapter feature parity **structural** instead of a
12
12
  > promise kept by hand.
13
13
 
@@ -33,6 +33,14 @@ supplies only the third:
33
33
  `Switch` is the canonical reference for a full three-layer component; `ActivityIndicator` is the
34
34
  canonical render-only reference (no state machine needed).
35
35
 
36
+ ### Install
37
+
38
+ Nearly every consumer gets this transitively, through an adapter. Building a new adapter yourself:
39
+
40
+ ```bash
41
+ npm install @symbiote-native/components
42
+ ```
43
+
36
44
  ---
37
45
 
38
46
  ## Usage
@@ -0,0 +1,9 @@
1
+ import { type IColorValue, type IDeviceEventSource, type INativeViewConfigSource } from '@symbiote-native/engine';
2
+ export type IBootstrapHostOptions = {
3
+ colorProcessor?: (value: IColorValue) => unknown;
4
+ imageSourceResolver?: (source: unknown) => unknown;
5
+ deviceEventSource?: IDeviceEventSource;
6
+ nativeViewConfigSource?: INativeViewConfigSource;
7
+ debug?: boolean;
8
+ };
9
+ export declare function bootstrapHost(options?: IBootstrapHostOptions): void;
@@ -0,0 +1,43 @@
1
+ // Zero-config host bootstrap for @symbiote-native/components' four RN-backed seams
2
+ // (colorProcessor, imageSourceResolver, deviceEventSource, nativeViewConfigSource), wired from
3
+ // real react-native in one call — collapses what every canary example currently hand-wires at
4
+ // startup. Lives OUTSIDE the package's main barrel (see package.json's separate "./bootstrap"
5
+ // export): react-native's own source is Flow syntax Vitest's transform can't parse, so anything
6
+ // importing it directly must stay unreachable from the tested main index.ts.
7
+ import { processColor, DeviceEventEmitter, Image } from 'react-native';
8
+ import { setColorProcessor, setDeviceEventSource, setNativeViewConfigSource, } from '@symbiote-native/engine';
9
+ import { setImageSourceResolver } from './view/render-image';
10
+ // @ts-expect-error react-native ships no types for this internal path (plain .js) — the
11
+ // try/catch below is what actually proves the shape, not TS.
12
+ import * as ReactNativeViewConfigRegistry from 'react-native/Libraries/Renderer/shims/ReactNativeViewConfigRegistry';
13
+ // Third-party Fabric views derive their events + prop processors from RN's own ViewConfig
14
+ // registry; `get` throws for an unregistered name, so undefined is the right answer for
15
+ // anything the registry doesn't know (our built-ins never reach here).
16
+ function defaultNativeViewConfigSource(name) {
17
+ try {
18
+ return ReactNativeViewConfigRegistry.get(name);
19
+ }
20
+ catch {
21
+ return undefined;
22
+ }
23
+ }
24
+ // require('./x.png') asset ids and {uri} sources are resolved by RN's own resolver before they
25
+ // reach the shared render fns. `source` is untyped at the setImageSourceResolver seam by design
26
+ // (any component's resolved shape flows through it) — this is the I/O edge where it crosses
27
+ // into RN's own typed Image API.
28
+ function defaultImageSourceResolver(source) {
29
+ return Image.resolveAssetSource(source);
30
+ }
31
+ // IColorValue is our own structural mirror of the runtime shapes RN's processColor accepts
32
+ // (CSS string / PlatformColor / DynamicColorIOS); RN's own ColorValue type is opaque, not
33
+ // structurally identical, so this is the I/O edge between the two color representations.
34
+ function defaultColorProcessor(value) {
35
+ return processColor(value);
36
+ }
37
+ export function bootstrapHost(options = {}) {
38
+ globalThis.__SYMBIOTE_DEBUG__ = options.debug ?? process.env.DEBUG === '1';
39
+ setColorProcessor(options.colorProcessor ?? defaultColorProcessor);
40
+ setImageSourceResolver(options.imageSourceResolver ?? defaultImageSourceResolver);
41
+ setDeviceEventSource(options.deviceEventSource ?? DeviceEventEmitter);
42
+ setNativeViewConfigSource(options.nativeViewConfigSource ?? defaultNativeViewConfigSource);
43
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@symbiote-native/components",
3
- "version": "0.1.0",
4
- "description": "Framework-agnostic component logic (state machines + render functions) for SymbioteJS — written once, inherited by every adapter (React, Vue, Angular, ...).",
3
+ "version": "0.2.0",
4
+ "description": "Framework-agnostic component logic (state machines + render functions) for SymbioteNative — written once, inherited by every adapter (React, Vue, Angular, ...).",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/OneEyed1366/symbiote-native.git",
@@ -20,6 +20,10 @@
20
20
  ".": {
21
21
  "types": "./build/index.d.ts",
22
22
  "default": "./build/index.js"
23
+ },
24
+ "./bootstrap": {
25
+ "types": "./build/bootstrap.d.ts",
26
+ "default": "./build/bootstrap.js"
23
27
  }
24
28
  },
25
29
  "files": [
@@ -29,7 +33,13 @@
29
33
  "access": "public"
30
34
  },
31
35
  "dependencies": {
32
- "@symbiote-native/engine": "0.1.0"
36
+ "@symbiote-native/engine": "0.1.1"
37
+ },
38
+ "peerDependencies": {
39
+ "react-native": ">=0.86"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^26.0.0"
33
43
  },
34
44
  "scripts": {
35
45
  "typecheck": "tsc --build",
@@ -1,22 +0,0 @@
1
- import { type ISymbioteEvent, type ISymbioteNode } from '@symbiote/engine';
2
- export type IDrawerPosition = 'left' | 'right';
3
- export type IDrawerLockMode = 'unlocked' | 'locked-closed' | 'locked-open';
4
- export type IKeyboardDismissMode = 'none' | 'on-drag';
5
- export type IDrawerState = 'Idle' | 'Dragging' | 'Settling';
6
- export interface IDrawerSlideEvent {
7
- offset: number;
8
- }
9
- export interface IDrawerLayoutAndroidHandle {
10
- openDrawer(): void;
11
- closeDrawer(): void;
12
- }
13
- export declare const DRAWER_VIEW_NAME = "AndroidDrawerLayout";
14
- export declare const OPEN_DRAWER_COMMAND = "openDrawer";
15
- export declare const CLOSE_DRAWER_COMMAND = "closeDrawer";
16
- export declare const DRAWER_STATES: ReadonlyArray<IDrawerState>;
17
- export declare const DEFAULT_DRAWER_BACKGROUND_COLOR = "white";
18
- export declare const DEFAULT_DRAWER_POSITION: IDrawerPosition;
19
- export declare function offsetFromSlide(event: ISymbioteEvent): number;
20
- export declare function stateFromChange(event: ISymbioteEvent): IDrawerState;
21
- export declare function dispatchDrawerCommand(node: ISymbioteNode | null, command: string): void;
22
- export declare function buildDrawerHandle(getNode: () => ISymbioteNode | null): IDrawerLayoutAndroidHandle;
@@ -1,53 +0,0 @@
1
- // DrawerLayoutAndroid: the framework-agnostic logic half. AndroidDrawerLayout is Android-only, so
2
- // this module holds only the platform-invariant contract every adapter shares — the drawer-position
3
- // / lock-mode / keyboard-dismiss / state TYPES, the native view + command NAMES, the slide/state
4
- // event NORMALIZATION, and the imperative open/close HANDLE. The adapter supplies only its lifecycle
5
- // (refs / reactivity) + the descriptor→element bridge; the view (style/prop) math lives in
6
- // view/render-drawer-layout-android.ts. No Platform.OS read; the adapter's filename selects the build
7
- // (ADR 0019). The wolf-tui twin shape is the shared state module pulled out of each reconciler.
8
- import { dispatchViewCommand, dlog, } from '@symbiote/engine';
9
- // The native view name registered by AndroidDrawerLayoutNativeComponent's
10
- // codegenNativeComponent('AndroidDrawerLayout'): the derive-by-default name (any non-`symbiote-*`
11
- // type flows through descriptorFor untouched, the engine deriving its events from the ViewConfig).
12
- export const DRAWER_VIEW_NAME = 'AndroidDrawerLayout';
13
- export const OPEN_DRAWER_COMMAND = 'openDrawer';
14
- export const CLOSE_DRAWER_COMMAND = 'closeDrawer';
15
- // RN's drawerState int -> string mapping (android DRAWER_STATES indexed by the native drawerState):
16
- // 0=Idle, 1=Dragging, 2=Settling.
17
- export const DRAWER_STATES = ['Idle', 'Dragging', 'Settling'];
18
- export const DEFAULT_DRAWER_BACKGROUND_COLOR = 'white';
19
- export const DEFAULT_DRAWER_POSITION = 'left';
20
- // Slide-event normalization: pull the native drag offset (RN onDrawerSlide nativeEvent.offset).
21
- export function offsetFromSlide(event) {
22
- const offset = event.nativeEvent.offset;
23
- return typeof offset === 'number' ? offset : 0;
24
- }
25
- // State-change normalization: map the native drawerState int onto its DRAWER_STATES label.
26
- export function stateFromChange(event) {
27
- const index = event.nativeEvent.drawerState;
28
- if (typeof index === 'number' && index >= 0 && index < DRAWER_STATES.length) {
29
- return DRAWER_STATES[index];
30
- }
31
- return 'Idle';
32
- }
33
- // Issue a drawer command against the committed host node, or log a silent no-op when there is no
34
- // node yet (the first render has not committed). Mirrors Switch's dispatchViewCommand path.
35
- export function dispatchDrawerCommand(node, command) {
36
- if (node === null) {
37
- dlog(`DrawerLayoutAndroid ${command} no-op: no committed host node`);
38
- return;
39
- }
40
- dlog(`DrawerLayoutAndroid dispatch ${command}`);
41
- dispatchViewCommand(node, command, []);
42
- }
43
- // The imperative handle is identical across adapters: openDrawer / closeDrawer dispatch the matching
44
- // view command on the SAME host node. Built once here; each adapter backs it with its lazy node
45
- // getter (React `() => ref.current`, Vue `() => nodeRef.value`), read on every call — the node is
46
- // null until the element commits, so an eager capture would freeze null and every command no-op.
47
- // The Vue twin of React's useImperativeHandle(ref, …); mirrors buildScrollViewHandle.
48
- export function buildDrawerHandle(getNode) {
49
- return {
50
- openDrawer: () => dispatchDrawerCommand(getNode(), OPEN_DRAWER_COMMAND),
51
- closeDrawer: () => dispatchDrawerCommand(getNode(), CLOSE_DRAWER_COMMAND),
52
- };
53
- }
@@ -1,23 +0,0 @@
1
- import type { IStyleProp, IViewStyle } from '@symbiote/engine';
2
- import { type IDrawerLockMode, type IDrawerPosition, type IKeyboardDismissMode } from '../state/drawer-layout-android';
3
- export declare const DRAWER_HOST_STYLE: Readonly<IViewStyle>;
4
- export declare const DRAWER_MAIN_SUBVIEW_STYLE: Readonly<IViewStyle>;
5
- export interface IDrawerLayoutResolveInput {
6
- drawerWidth?: number;
7
- drawerPosition?: IDrawerPosition;
8
- drawerLockMode?: IDrawerLockMode;
9
- keyboardDismissMode?: IKeyboardDismissMode;
10
- drawerBackgroundColor?: string;
11
- statusBarBackgroundColor?: string;
12
- drawerOpened: boolean;
13
- style?: IStyleProp<IViewStyle>;
14
- passthrough: Record<string, unknown>;
15
- }
16
- export interface IDrawerLayoutResolved {
17
- viewName: string;
18
- hostProps: Record<string, unknown>;
19
- contentWrapperStyle: Readonly<IViewStyle>;
20
- navigationWrapperStyle: IViewStyle;
21
- navigationPointerEvents: 'auto' | 'none';
22
- }
23
- export declare function resolveDrawerLayout(input: IDrawerLayoutResolveInput): IDrawerLayoutResolved;
@@ -1,56 +0,0 @@
1
- // DrawerLayoutAndroid: the framework-agnostic view (style/prop) math. AndroidDrawerLayout is an
2
- // ordinary Fabric host node committing through the same childSet as the rest of the tree (like
3
- // Modal / Switch): a content wrapper FIRST and a navigation wrapper SECOND, mirroring RN's android
4
- // render order {childrenWrapper}{drawerViewWrapper}. This builds the host prop bag + the two wrapper
5
- // styles from the resolved props; the adapter overlays its ref + the wrapped event handlers and
6
- // supplies the framework children / navigation elements. The logic half (event normalization,
7
- // imperative handle, constants, types) lives in state/drawer-layout-android.ts.
8
- import { DEFAULT_DRAWER_BACKGROUND_COLOR, DEFAULT_DRAWER_POSITION, DRAWER_VIEW_NAME, } from '../state/drawer-layout-android';
9
- // RN styles.base on the host: flex:1 plus the Android drop-shadow that floats the drawer over
10
- // content (android styles.base { flex:1, elevation:16 }).
11
- export const DRAWER_HOST_STYLE = {
12
- flex: 1,
13
- elevation: 16,
14
- };
15
- // RN styles.mainSubview: the content wrapper fills the host (absolute, all edges 0).
16
- export const DRAWER_MAIN_SUBVIEW_STYLE = {
17
- position: 'absolute',
18
- top: 0,
19
- left: 0,
20
- right: 0,
21
- bottom: 0,
22
- };
23
- // RN styles.drawerSubview: the navigation wrapper is absolute and full-height; its width comes from
24
- // drawerWidth and its background from drawerBackgroundColor (both folded in by resolveDrawerLayout).
25
- const DRAWER_SUBVIEW_STYLE = {
26
- position: 'absolute',
27
- top: 0,
28
- bottom: 0,
29
- };
30
- // Build the AndroidDrawerLayout host prop bag + the two wrapper styles from the resolved props. The
31
- // adapter overlays its ref + the four wrapped event handlers (onDrawerOpen / Close / Slide /
32
- // StateChanged) and nests [contentWrapper, navigationWrapper] under the host IN THAT ORDER.
33
- export function resolveDrawerLayout(input) {
34
- const drawerBackgroundColor = input.drawerBackgroundColor ?? DEFAULT_DRAWER_BACKGROUND_COLOR;
35
- const hostProps = {
36
- ...input.passthrough,
37
- drawerWidth: input.drawerWidth,
38
- drawerPosition: input.drawerPosition ?? DEFAULT_DRAWER_POSITION,
39
- drawerLockMode: input.drawerLockMode,
40
- keyboardDismissMode: input.keyboardDismissMode,
41
- drawerBackgroundColor,
42
- statusBarBackgroundColor: input.statusBarBackgroundColor,
43
- style: [DRAWER_HOST_STYLE, input.style],
44
- };
45
- return {
46
- viewName: DRAWER_VIEW_NAME,
47
- hostProps,
48
- contentWrapperStyle: DRAWER_MAIN_SUBVIEW_STYLE,
49
- navigationWrapperStyle: {
50
- ...DRAWER_SUBVIEW_STYLE,
51
- width: input.drawerWidth,
52
- backgroundColor: drawerBackgroundColor,
53
- },
54
- navigationPointerEvents: input.drawerOpened ? 'auto' : 'none',
55
- };
56
- }