@symbiote-native/engine 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 (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -0
  3. package/build/accessibility-info/index.android.d.ts +3 -0
  4. package/build/accessibility-info/index.android.js +166 -0
  5. package/build/accessibility-info/index.d.ts +1 -0
  6. package/build/accessibility-info/index.ios.d.ts +3 -0
  7. package/build/accessibility-info/index.ios.js +219 -0
  8. package/build/accessibility-info/index.js +5 -0
  9. package/build/accessibility-info/shared.d.ts +34 -0
  10. package/build/accessibility-info/shared.js +13 -0
  11. package/build/action-sheet-ios/index.d.ts +36 -0
  12. package/build/action-sheet-ios/index.js +74 -0
  13. package/build/alert/index.android.d.ts +5 -0
  14. package/build/alert/index.android.js +117 -0
  15. package/build/alert/index.d.ts +1 -0
  16. package/build/alert/index.ios.d.ts +7 -0
  17. package/build/alert/index.ios.js +83 -0
  18. package/build/alert/index.js +8 -0
  19. package/build/alert/shared.d.ts +19 -0
  20. package/build/alert/shared.js +17 -0
  21. package/build/animated/animated-component-shared.d.ts +5 -0
  22. package/build/animated/animated-component-shared.js +54 -0
  23. package/build/animated/animation.d.ts +9 -0
  24. package/build/animated/animation.js +6 -0
  25. package/build/animated/animations/base.d.ts +27 -0
  26. package/build/animated/animations/base.js +90 -0
  27. package/build/animated/animations/composition.d.ts +38 -0
  28. package/build/animated/animations/composition.js +236 -0
  29. package/build/animated/animations/decay.d.ts +22 -0
  30. package/build/animated/animations/decay.js +65 -0
  31. package/build/animated/animations/raf.d.ts +5 -0
  32. package/build/animated/animations/raf.js +39 -0
  33. package/build/animated/animations/spring-config.d.ts +6 -0
  34. package/build/animated/animations/spring-config.js +55 -0
  35. package/build/animated/animations/spring.d.ts +50 -0
  36. package/build/animated/animations/spring.js +207 -0
  37. package/build/animated/animations/timing.d.ts +27 -0
  38. package/build/animated/animations/timing.js +101 -0
  39. package/build/animated/animations/tracking.d.ts +14 -0
  40. package/build/animated/animations/tracking.js +43 -0
  41. package/build/animated/bezier.d.ts +1 -0
  42. package/build/animated/bezier.js +101 -0
  43. package/build/animated/color.d.ts +37 -0
  44. package/build/animated/color.js +183 -0
  45. package/build/animated/easing.d.ts +20 -0
  46. package/build/animated/easing.js +96 -0
  47. package/build/animated/event.d.ts +36 -0
  48. package/build/animated/event.js +252 -0
  49. package/build/animated/graph.d.ts +38 -0
  50. package/build/animated/graph.js +227 -0
  51. package/build/animated/index.d.ts +20 -0
  52. package/build/animated/index.js +28 -0
  53. package/build/animated/interpolation-node.d.ts +16 -0
  54. package/build/animated/interpolation-node.js +57 -0
  55. package/build/animated/interpolation.d.ts +22 -0
  56. package/build/animated/interpolation.js +199 -0
  57. package/build/animated/mock.d.ts +56 -0
  58. package/build/animated/mock.js +127 -0
  59. package/build/animated/native/native-animated.d.ts +43 -0
  60. package/build/animated/native/native-animated.js +146 -0
  61. package/build/animated/operators.d.ts +80 -0
  62. package/build/animated/operators.js +266 -0
  63. package/build/animated/props.d.ts +20 -0
  64. package/build/animated/props.js +187 -0
  65. package/build/animated/style.d.ts +26 -0
  66. package/build/animated/style.js +187 -0
  67. package/build/animated/value-xy.d.ts +35 -0
  68. package/build/animated/value-xy.js +106 -0
  69. package/build/animated/value.d.ts +36 -0
  70. package/build/animated/value.js +185 -0
  71. package/build/app-registry/index.d.ts +40 -0
  72. package/build/app-registry/index.js +144 -0
  73. package/build/app-state/index.d.ts +16 -0
  74. package/build/app-state/index.js +105 -0
  75. package/build/appearance/index.d.ts +12 -0
  76. package/build/appearance/index.js +84 -0
  77. package/build/back-handler/index.d.ts +14 -0
  78. package/build/back-handler/index.js +106 -0
  79. package/build/commit.d.ts +16 -0
  80. package/build/commit.js +678 -0
  81. package/build/debug.d.ts +5 -0
  82. package/build/debug.js +18 -0
  83. package/build/dimensions/index.d.ts +28 -0
  84. package/build/dimensions/index.js +148 -0
  85. package/build/dispatch.d.ts +2 -0
  86. package/build/dispatch.js +18 -0
  87. package/build/events/index.d.ts +1 -0
  88. package/build/events/index.js +691 -0
  89. package/build/fabric.d.ts +32 -0
  90. package/build/fabric.js +59 -0
  91. package/build/host-instance/index.d.ts +11 -0
  92. package/build/host-instance/index.js +49 -0
  93. package/build/i18n-manager/index.d.ts +13 -0
  94. package/build/i18n-manager/index.js +91 -0
  95. package/build/index.d.ts +80 -0
  96. package/build/index.js +72 -0
  97. package/build/interaction-manager/index.d.ts +45 -0
  98. package/build/interaction-manager/index.js +222 -0
  99. package/build/keyboard/index.d.ts +31 -0
  100. package/build/keyboard/index.js +142 -0
  101. package/build/layout-animation/index.d.ts +66 -0
  102. package/build/layout-animation/index.js +183 -0
  103. package/build/linking/index.android.d.ts +2 -0
  104. package/build/linking/index.android.js +18 -0
  105. package/build/linking/index.d.ts +1 -0
  106. package/build/linking/index.ios.d.ts +2 -0
  107. package/build/linking/index.ios.js +9 -0
  108. package/build/linking/index.js +6 -0
  109. package/build/linking/shared.d.ts +32 -0
  110. package/build/linking/shared.js +98 -0
  111. package/build/native-events.d.ts +24 -0
  112. package/build/native-events.js +129 -0
  113. package/build/native-modules.d.ts +6 -0
  114. package/build/native-modules.js +57 -0
  115. package/build/node.d.ts +36 -0
  116. package/build/node.js +194 -0
  117. package/build/pan-responder/index.d.ts +53 -0
  118. package/build/pan-responder/index.js +353 -0
  119. package/build/permissions-android/index.d.ts +115 -0
  120. package/build/permissions-android/index.js +185 -0
  121. package/build/pixel-ratio/index.d.ts +8 -0
  122. package/build/pixel-ratio/index.js +27 -0
  123. package/build/platform/index.android.d.ts +22 -0
  124. package/build/platform/index.android.js +60 -0
  125. package/build/platform/index.d.ts +1 -0
  126. package/build/platform/index.ios.d.ts +18 -0
  127. package/build/platform/index.ios.js +62 -0
  128. package/build/platform/index.js +5 -0
  129. package/build/platform/shared.d.ts +25 -0
  130. package/build/platform/shared.js +41 -0
  131. package/build/platform-color.d.ts +19 -0
  132. package/build/platform-color.js +25 -0
  133. package/build/post-commit.d.ts +4 -0
  134. package/build/post-commit.js +16 -0
  135. package/build/process-aspect-ratio.d.ts +1 -0
  136. package/build/process-aspect-ratio.js +34 -0
  137. package/build/process-background-image/index.d.ts +28 -0
  138. package/build/process-background-image/index.js +557 -0
  139. package/build/process-box-shadow/index.d.ts +11 -0
  140. package/build/process-box-shadow/index.js +193 -0
  141. package/build/process-filter.d.ts +31 -0
  142. package/build/process-filter.js +304 -0
  143. package/build/process-font-variant.d.ts +1 -0
  144. package/build/process-font-variant.js +17 -0
  145. package/build/process-transform/index.d.ts +5 -0
  146. package/build/process-transform/index.js +120 -0
  147. package/build/process-transform-origin/index.d.ts +3 -0
  148. package/build/process-transform-origin/index.js +108 -0
  149. package/build/registry.d.ts +31 -0
  150. package/build/registry.js +145 -0
  151. package/build/settings/index.d.ts +8 -0
  152. package/build/settings/index.js +126 -0
  153. package/build/share/index.android.d.ts +3 -0
  154. package/build/share/index.android.js +56 -0
  155. package/build/share/index.d.ts +1 -0
  156. package/build/share/index.ios.d.ts +3 -0
  157. package/build/share/index.ios.js +47 -0
  158. package/build/share/index.js +6 -0
  159. package/build/share/shared.d.ts +32 -0
  160. package/build/share/shared.js +32 -0
  161. package/build/status-bar/index.android.d.ts +5 -0
  162. package/build/status-bar/index.android.js +83 -0
  163. package/build/status-bar/index.d.ts +1 -0
  164. package/build/status-bar/index.ios.d.ts +5 -0
  165. package/build/status-bar/index.ios.js +66 -0
  166. package/build/status-bar/index.js +4 -0
  167. package/build/status-bar/shared.d.ts +22 -0
  168. package/build/status-bar/shared.js +22 -0
  169. package/build/style/index.d.ts +1 -0
  170. package/build/style/index.js +30 -0
  171. package/build/style-registry/index.d.ts +11 -0
  172. package/build/style-registry/index.js +165 -0
  173. package/build/style-sheet/index.d.ts +20 -0
  174. package/build/style-sheet/index.js +121 -0
  175. package/build/styles.d.ts +220 -0
  176. package/build/styles.js +7 -0
  177. package/build/surface.d.ts +16 -0
  178. package/build/surface.js +67 -0
  179. package/build/tags.d.ts +1 -0
  180. package/build/tags.js +10 -0
  181. package/build/text-input-state.d.ts +5 -0
  182. package/build/text-input-state.js +29 -0
  183. package/build/toast-android/index.d.ts +10 -0
  184. package/build/toast-android/index.js +108 -0
  185. package/build/vibration/index.android.d.ts +2 -0
  186. package/build/vibration/index.android.js +18 -0
  187. package/build/vibration/index.d.ts +1 -0
  188. package/build/vibration/index.ios.d.ts +2 -0
  189. package/build/vibration/index.ios.js +54 -0
  190. package/build/vibration/index.js +6 -0
  191. package/build/vibration/shared.d.ts +15 -0
  192. package/build/vibration/shared.js +68 -0
  193. package/build/view-config.d.ts +1 -0
  194. package/build/view-config.js +114 -0
  195. package/package.json +41 -0
@@ -0,0 +1,83 @@
1
+ // StatusBar on Android: drives the Android StatusBarManager from props (via
2
+ // applyStatusBarProps) and from the static methods (statusBarImperative). Android's native
3
+ // module is a DIFFERENT shape from iOS: single-arg setHidden(hidden) / setStyle(style) plus
4
+ // setColor/setTranslucent, same module name ('StatusBarManager'). Metro picks this on an
5
+ // Android host; iOS keeps its own shape. Each adapter wraps these with its declarative component.
6
+ //
7
+ // History: this used to be a no-op. Driving the window flags from our bridgeless surface
8
+ // blanked the app: a status-bar relayout triggered stopSurface, which threw "Global was
9
+ // not installed" because RN installs global.RN$stopSurface from its own renderer, which we
10
+ // replace. Now that render.ts installs RN$stopSurface and tears surfaces down cleanly, the
11
+ // relayout survives and the bar updates without blanking (verified on device: show/hide +
12
+ // light/dark text). See render.ts installStopSurfaceGlobal + native-module-platform-routing.
13
+ import { getNativeModule } from '../native-modules';
14
+ import { processColor } from '../commit';
15
+ import { dlog } from '../debug';
16
+ import { STATUS_BAR_MANAGER } from './shared';
17
+ // processColor returns `unknown` (its result is platform-dependent); narrow to the
18
+ // number Fabric/native expects, like RN's invariant before setColor. A non-number
19
+ // (null for an unparseable color, headless identity passthrough of a string) is dropped.
20
+ function applyBackgroundColor(manager, color, animated) {
21
+ const processed = processColor(color);
22
+ if (typeof processed !== 'number') {
23
+ dlog(`StatusBar android: backgroundColor ${String(color)} did not process to an int — skipping`);
24
+ return;
25
+ }
26
+ // RISK: driving the window background flag once blanked the bridgeless surface
27
+ // (see header). Routed cleanly here but DEVICE-VERIFY-PENDING.
28
+ dlog(`StatusBar android setColor -> ${processed} animated=${animated}`);
29
+ manager.setColor(processed, animated);
30
+ }
31
+ // Apply a StatusBar component's props to the native module, same contract as iOS, with the
32
+ // single-arg Android setters. The adapter calls this from its declarative component's effect,
33
+ // on mount and on every prop change.
34
+ export function applyStatusBarProps(props) {
35
+ const { barStyle, hidden, animated = false, backgroundColor, translucent } = props;
36
+ const manager = getNativeModule(STATUS_BAR_MANAGER);
37
+ if (manager === null) {
38
+ dlog('StatusBar android: StatusBarManager not resolvable — skipping');
39
+ return;
40
+ }
41
+ dlog(`StatusBar android -> barStyle=${barStyle} hidden=${hidden} translucent=${translucent} bg=${String(backgroundColor)}`);
42
+ if (barStyle !== undefined)
43
+ manager.setStyle(barStyle);
44
+ if (hidden !== undefined)
45
+ manager.setHidden(hidden);
46
+ if (translucent !== undefined)
47
+ manager.setTranslucent(translucent);
48
+ if (backgroundColor !== undefined)
49
+ applyBackgroundColor(manager, backgroundColor, animated);
50
+ }
51
+ export const statusBarImperative = {
52
+ setBarStyle(style) {
53
+ dlog(`StatusBar.setBarStyle android ${style}`);
54
+ getNativeModule(STATUS_BAR_MANAGER)?.setStyle(style);
55
+ },
56
+ setHidden(hidden) {
57
+ dlog(`StatusBar.setHidden android ${hidden}`);
58
+ getNativeModule(STATUS_BAR_MANAGER)?.setHidden(hidden);
59
+ },
60
+ setNetworkActivityIndicatorVisible() {
61
+ // No Android equivalent: an iOS-only concept.
62
+ dlog('StatusBar.setNetworkActivityIndicatorVisible (android no-op)');
63
+ },
64
+ setBackgroundColor(color, animated = false) {
65
+ dlog(`StatusBar.setBackgroundColor android ${String(color)}`);
66
+ const manager = getNativeModule(STATUS_BAR_MANAGER);
67
+ if (manager === null)
68
+ return;
69
+ applyBackgroundColor(manager, color, animated);
70
+ },
71
+ setTranslucent(translucent) {
72
+ // RISK: window translucent flag is device-verify-pending (may blank the surface).
73
+ dlog(`StatusBar.setTranslucent android ${translucent}`);
74
+ getNativeModule(STATUS_BAR_MANAGER)?.setTranslucent(translucent);
75
+ },
76
+ };
77
+ // Android exposes the bar height as a native constant; undefined if the module or the
78
+ // constant is absent (older RN, or a fake that doesn't define getConstants). Read lazily
79
+ // so nothing touches native at import time.
80
+ export function statusBarCurrentHeight() {
81
+ return getNativeModule(STATUS_BAR_MANAGER)?.getConstants?.()
82
+ .HEIGHT;
83
+ }
@@ -0,0 +1 @@
1
+ export * from './index.ios';
@@ -0,0 +1,5 @@
1
+ import { type IStatusBarImperative, type IStatusBarProps } from './shared';
2
+ export type { IStatusBarProps, IStatusBarStyle } from './shared';
3
+ export declare function applyStatusBarProps(props: IStatusBarProps): void;
4
+ export declare const statusBarImperative: IStatusBarImperative;
5
+ export declare function statusBarCurrentHeight(): number | undefined;
@@ -0,0 +1,66 @@
1
+ // StatusBar on iOS drives the iOS `StatusBarManager` TurboModule from props (via
2
+ // applyStatusBarProps) and from the static methods (statusBarImperative). The native
3
+ // contract is RN's spec at
4
+ // .vendors/react-native/.../src/private/specs_DEPRECATED/modules/NativeStatusBarManagerIOS.js:
5
+ // setStyle(statusBarStyle?: string, animated: boolean)
6
+ // setHidden(hidden: boolean, withAnimation: 'none' | 'fade' | 'slide')
7
+ // setNetworkActivityIndicatorVisible(visible: boolean)
8
+ // We mirror only those three setters as our hand-written interface: the typed trust
9
+ // boundary getNativeModule<T> resolves against. Metro picks this on an iOS host; the base
10
+ // status-bar.ts re-exports it for tsc / tsx / headless. Each adapter wraps these with its own
11
+ // declarative component.
12
+ import { getNativeModule } from '../native-modules';
13
+ import { dlog } from '../debug';
14
+ import { STATUS_BAR_MANAGER, STATIC_HIDE_TRANSITION, hideTransition, } from './shared';
15
+ // Apply a StatusBar component's props to the native module. The adapter calls this from its
16
+ // declarative component's effect, on mount and on every prop change. Resolves lazily inside
17
+ // the call, not at import; keeps this module importable headless before a fake
18
+ // __turboModuleProxy is installed. Non-enforcing: a declarative StatusBar must NOT crash the
19
+ // whole render if the module can't resolve.
20
+ //
21
+ // Simplification vs RN: RN maintains a prop-merge stack so nested StatusBars compose
22
+ // (deepest/last wins); we direct-apply a single component's props, which is correct for one
23
+ // StatusBar and a fine first cut.
24
+ export function applyStatusBarProps(props) {
25
+ const { barStyle, hidden, animated = false, networkActivityIndicatorVisible } = props;
26
+ const manager = getNativeModule(STATUS_BAR_MANAGER);
27
+ if (manager === null) {
28
+ dlog('StatusBar: StatusBarManager not resolvable via __turboModuleProxy — skipping');
29
+ return;
30
+ }
31
+ dlog('StatusBar -> applying props to StatusBarManager');
32
+ if (barStyle !== undefined)
33
+ manager.setStyle(barStyle, animated);
34
+ if (hidden !== undefined)
35
+ manager.setHidden(hidden, hideTransition(animated));
36
+ if (networkActivityIndicatorVisible !== undefined) {
37
+ manager.setNetworkActivityIndicatorVisible(networkActivityIndicatorVisible);
38
+ }
39
+ }
40
+ // The static API mirrors the declarative component: non-throwing, a missing optional
41
+ // native module is a no-op, never a crash. Android-only setters are inert on iOS (the iOS
42
+ // status bar has no background color and is never translucent in RN's sense).
43
+ export const statusBarImperative = {
44
+ setBarStyle(style, animated = false) {
45
+ dlog('StatusBar.setBarStyle');
46
+ getNativeModule(STATUS_BAR_MANAGER)?.setStyle(style, animated);
47
+ },
48
+ setHidden(hidden, animation = STATIC_HIDE_TRANSITION) {
49
+ dlog('StatusBar.setHidden');
50
+ getNativeModule(STATUS_BAR_MANAGER)?.setHidden(hidden, animation);
51
+ },
52
+ setNetworkActivityIndicatorVisible(visible) {
53
+ dlog('StatusBar.setNetworkActivityIndicatorVisible');
54
+ getNativeModule(STATUS_BAR_MANAGER)?.setNetworkActivityIndicatorVisible(visible);
55
+ },
56
+ setBackgroundColor() {
57
+ dlog('StatusBar.setBackgroundColor (ios no-op)');
58
+ },
59
+ setTranslucent() {
60
+ dlog('StatusBar.setTranslucent (ios no-op)');
61
+ },
62
+ };
63
+ // currentHeight is Android-only; absent on iOS (RN sets it to null on iOS).
64
+ export function statusBarCurrentHeight() {
65
+ return undefined;
66
+ }
@@ -0,0 +1,4 @@
1
+ // Base / default StatusBar engine surface; re-exports the iOS build. Metro overrides this
2
+ // with status-bar.ios.ts / .android.ts on a real host; under tsx / tsc / web the host config
3
+ // resolves here. Filename is the selector, no Platform.OS read. See status-bar-shared.ts.
4
+ export * from './index.ios';
@@ -0,0 +1,22 @@
1
+ import type { IColorValue } from '../platform-color';
2
+ export type IStatusBarStyle = 'default' | 'light-content' | 'dark-content';
3
+ export type IStatusBarAnimation = 'none' | 'fade' | 'slide';
4
+ export declare const STATUS_BAR_MANAGER = "StatusBarManager";
5
+ export declare const ANIMATED_HIDE_TRANSITION: IStatusBarAnimation;
6
+ export declare const STATIC_HIDE_TRANSITION: IStatusBarAnimation;
7
+ export declare function hideTransition(animated: boolean): IStatusBarAnimation;
8
+ export interface IStatusBarProps {
9
+ barStyle?: IStatusBarStyle;
10
+ hidden?: boolean;
11
+ animated?: boolean;
12
+ networkActivityIndicatorVisible?: boolean;
13
+ backgroundColor?: IColorValue;
14
+ translucent?: boolean;
15
+ }
16
+ export interface IStatusBarImperative {
17
+ setBarStyle(style: IStatusBarStyle, animated?: boolean): void;
18
+ setHidden(hidden: boolean, animation?: IStatusBarAnimation): void;
19
+ setNetworkActivityIndicatorVisible(visible: boolean): void;
20
+ setBackgroundColor(color: IColorValue, animated?: boolean): void;
21
+ setTranslucent(translucent: boolean): void;
22
+ }
@@ -0,0 +1,22 @@
1
+ // StatusBar: shared contract. The component renders NO Fabric view; it imperatively
2
+ // drives a status-bar native module. What DIVERGES by platform is the native module's
3
+ // method shape: iOS's StatusBarManager takes `setStyle(style, animated)` /
4
+ // `setHidden(hidden, withAnimation)`, while Android's takes single-arg `setStyle(style)` /
5
+ // `setHidden(hidden)` plus `setColor` / `setTranslucent`, and driving those Android window
6
+ // flags from our bridgeless surface blanks it (a window-insets relayout detaches the Fabric
7
+ // surface). So the .ios/.android files own the native calls (applyStatusBarProps +
8
+ // statusBarImperative); the types + the framework-agnostic imperative surface live here.
9
+ // Filename selects, no Platform.OS read (see ADR 0012 + native_module_name_is_platform_specific).
10
+ //
11
+ // This is the engine half: pure types + the imperative API. Each adapter wraps it with a
12
+ // per-framework declarative component (React FC + useEffect, Vue defineComponent + watchEffect)
13
+ // that renders null and applies the props through applyStatusBarProps. The imperative API is
14
+ // shared verbatim: a single StatusBarManager driver behind both adapters.
15
+ export const STATUS_BAR_MANAGER = 'StatusBarManager';
16
+ // RN's default hide/show transition when `animated` is true (showHideTransition
17
+ // defaults to 'fade'); 'none' otherwise.
18
+ export const ANIMATED_HIDE_TRANSITION = 'fade';
19
+ export const STATIC_HIDE_TRANSITION = 'none';
20
+ export function hideTransition(animated) {
21
+ return animated ? ANIMATED_HIDE_TRANSITION : STATIC_HIDE_TRANSITION;
22
+ }
@@ -0,0 +1 @@
1
+ export declare function flattenStyle(style: unknown): Record<string, unknown>;
@@ -0,0 +1,30 @@
1
+ // StyleSheet.flatten, ported. RN's idiom is `style={[base, override, cond && extra]}`,
2
+ // an array of objects (and nested arrays) where later keys win. But Fabric's C++
3
+ // reads a single flat props payload, so before we diff and commit we must collapse
4
+ // that array into one plain object. This is the only place that collapse happens.
5
+ //
6
+ // Mirrors react-native/Libraries/StyleSheet/flattenStyle.js: recurse on the style
7
+ // POSITION only, never on a property VALUE. `transform: [{translateX: 5}]` is an
8
+ // array-valued prop and `shadowOffset: {width, height}` an object-valued prop; both
9
+ // are copied through untouched. Only the top-level style slot is flattened.
10
+ function isPlainObject(value) {
11
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
12
+ }
13
+ export function flattenStyle(style) {
14
+ if (Array.isArray(style)) {
15
+ const result = {};
16
+ for (const entry of style) {
17
+ // Falsy entries (null/undefined/false/''/0) flatten to {} and contribute nothing.
18
+ const flat = flattenStyle(entry);
19
+ for (const key in flat) {
20
+ result[key] = flat[key];
21
+ }
22
+ }
23
+ return result;
24
+ }
25
+ if (isPlainObject(style)) {
26
+ // Shallow copy of own enumerable keys: values (arrays, nested objects) pass through.
27
+ return { ...style };
28
+ }
29
+ return {};
30
+ }
@@ -0,0 +1,11 @@
1
+ import type { IViewStyle, ITextStyle } from '../styles';
2
+ type IResolvedStyle = Partial<IViewStyle & ITextStyle>;
3
+ export type IClassNameValue = string | IResolvedStyle | Array<string | IResolvedStyle> | undefined | null;
4
+ export declare function registerStyles(styles: Record<string, IResolvedStyle>): void;
5
+ export declare function clearGlobalStyles(): void;
6
+ export declare function isClassNameValue(value: unknown): value is IClassNameValue;
7
+ export declare function resolveClassName(className: IClassNameValue): IResolvedStyle;
8
+ export type IClassToggleMap = Record<string, boolean | undefined>;
9
+ export type IScopableClassValue = string | IClassToggleMap | Array<string | IClassToggleMap> | undefined | null;
10
+ export declare function scopeClassName(value: IScopableClassValue, localNames: ReadonlySet<string>, scopeId: string): IScopableClassValue;
11
+ export {};
@@ -0,0 +1,165 @@
1
+ // Runtime style registry, ported from wolf-tui's internal/shared/src/styles/registry.ts.
2
+ // Side-effect CSS imports (compiled by the sibling CSS-to-style build package, not
3
+ // this module) call registerStyles() with camelCase keys; components look them up
4
+ // by resolveClassName(). No CSS parsing happens here — this is a Map<string, ...>
5
+ // lookup, nothing more.
6
+ //
7
+ // Dropped vs the wolf-tui original: all Tailwind-utility detection (registerTailwind-
8
+ // Metadata / isTailwindUtility / custom prefix-static sets) — this repo's style
9
+ // surface has no Tailwind layer, so compound lookup below always runs for 2-4-part
10
+ // class strings instead of being gated behind "no part looks like a utility class".
11
+ //
12
+ // kebab-case authoring: a CSS selector `.section-label` always registers under the
13
+ // camelCase key `sectionLabel` (@symbiote-native/css-parser's extractClassName), so a template
14
+ // can write EITHER `class="sectionLabel"` OR `class="section-label"` — resolveOne below
15
+ // falls back to the kebab->camel form on a miss. Reinstated (wolf-tui had this, an
16
+ // earlier port here dropped it on the assumption authors would always match the
17
+ // camelCase key exactly) once that assumption proved wrong in practice.
18
+ // Compound lookup tries every ordering of 2-4 space-separated class parts joined by
19
+ // '.' (e.g. "btn primary" -> "btn.primary" / "primary.btn") before falling back to a
20
+ // per-class merge, mirroring CSS compound-selector registration (`.btn.primary { }`).
21
+ const COMPOUND_MIN_PARTS = 2;
22
+ const COMPOUND_MAX_PARTS = 4;
23
+ const globalStyles = new Map();
24
+ // Called by generated code from side-effect style imports. Last import wins on a
25
+ // name collision, matching CSS cascade behavior.
26
+ export function registerStyles(styles) {
27
+ for (const [name, style] of Object.entries(styles)) {
28
+ globalStyles.set(name, style);
29
+ }
30
+ }
31
+ // Clears every registration; used between tests for isolation.
32
+ export function clearGlobalStyles() {
33
+ globalStyles.clear();
34
+ }
35
+ // A `class`/`className` prop arrives as `unknown` at the routeProp boundary (any adapter can
36
+ // hand over anything); this narrows before resolveClassName without an `as` cast. Shared with
37
+ // adapters/vue/src/components/scroll-view/shared.ts's identical need, rather than each keeping
38
+ // its own copy — that file imports this one instead of redeclaring it.
39
+ export function isClassNameValue(value) {
40
+ return typeof value === 'string' || (typeof value === 'object' && value !== null);
41
+ }
42
+ export function resolveClassName(className) {
43
+ if (!className)
44
+ return {};
45
+ if (typeof className === 'object' && !Array.isArray(className)) {
46
+ return className;
47
+ }
48
+ if (Array.isArray(className)) {
49
+ return className.reduce((acc, item) => {
50
+ return { ...acc, ...resolveClassName(item) };
51
+ }, {});
52
+ }
53
+ const trimmed = className.trim();
54
+ if (!trimmed)
55
+ return {};
56
+ const exactMatch = globalStyles.get(trimmed) ?? globalStyles.get(kebabToCamel(trimmed));
57
+ if (exactMatch)
58
+ return exactMatch;
59
+ const parts = trimmed.split(/\s+/).filter(Boolean);
60
+ if (parts.length >= COMPOUND_MIN_PARTS && parts.length <= COMPOUND_MAX_PARTS) {
61
+ const compound = tryCompoundLookup(parts);
62
+ if (compound)
63
+ return compound;
64
+ }
65
+ return parts.reduce((acc, cls) => {
66
+ return { ...acc, ...resolveOne(cls) };
67
+ }, {});
68
+ }
69
+ function generateCompoundPermutations(parts) {
70
+ if (parts.length < COMPOUND_MIN_PARTS)
71
+ return [];
72
+ const compounds = [];
73
+ for (let size = COMPOUND_MIN_PARTS; size <= parts.length; size++) {
74
+ compounds.push(...generateKPermutations(parts, size));
75
+ }
76
+ return compounds;
77
+ }
78
+ function generateKPermutations(parts, size) {
79
+ if (size === 0)
80
+ return [''];
81
+ if (parts.length === 0)
82
+ return [];
83
+ const result = [];
84
+ function helper(current, remaining, depth) {
85
+ if (depth === size) {
86
+ result.push(toCompoundKey(current));
87
+ return;
88
+ }
89
+ for (let i = 0; i < remaining.length; i++) {
90
+ helper([...current, remaining[i]], remaining.slice(0, i).concat(remaining.slice(i + 1)), depth + 1);
91
+ }
92
+ }
93
+ helper([], parts, 0);
94
+ return result;
95
+ }
96
+ // wolf-tui joins a compound permutation with '.' ("btn.primary") because its CSS
97
+ // pipeline registers dot-joined selector strings. This repo's CSS-to-style compiler
98
+ // emits plain camelCase keys for every class, single or compound, so "btn primary"
99
+ // must resolve against a registered "btnPrimary" instead.
100
+ function toCompoundKey(parts) {
101
+ return parts.reduce((key, part, index) => (index === 0 ? part : key + capitalize(part)), '');
102
+ }
103
+ function capitalize(value) {
104
+ return value.length === 0 ? value : value[0].toUpperCase() + value.slice(1);
105
+ }
106
+ // Duplicated from @symbiote-native/css-parser's identical helper rather than imported: css-parser
107
+ // pulls in postcss and is build-time only (never shipped in the app bundle), and this registry
108
+ // is the opposite — pure runtime, in every app bundle — so importing it here would leak a
109
+ // build-time dependency into the shipped app. The conversion itself is two lines; keeping both
110
+ // copies in sync is a smaller cost than the alternative.
111
+ function kebabToCamel(value) {
112
+ return value.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
113
+ }
114
+ function tryCompoundLookup(parts) {
115
+ if (parts.length < COMPOUND_MIN_PARTS)
116
+ return null;
117
+ for (const compound of generateCompoundPermutations(parts)) {
118
+ const style = globalStyles.get(compound);
119
+ if (style)
120
+ return style;
121
+ }
122
+ return null;
123
+ }
124
+ function resolveOne(name) {
125
+ const trimmed = name.trim();
126
+ if (!trimmed)
127
+ return {};
128
+ return globalStyles.get(trimmed) ?? globalStyles.get(kebabToCamel(trimmed)) ?? {};
129
+ }
130
+ // Pure NAME rewriter for Vue `<style scoped>`: suffixes every class token that this
131
+ // file's scoped block locally defines with `__${scopeId}`, leaving unrecognized tokens
132
+ // (globals, external classes) untouched. Runs at the compiled call site BEFORE Vue's own
133
+ // normalizeClass() collapses string/object/array `class` values to a final string, so it
134
+ // must pre-process all three shapes normalizeClass understands. No registry lookup here —
135
+ // resolveClassName still does the actual style lookup, unchanged, against the rewritten name.
136
+ export function scopeClassName(value, localNames, scopeId) {
137
+ if (value === undefined || value === null)
138
+ return value;
139
+ if (Array.isArray(value)) {
140
+ return value.map(item => scopeClassEntry(item, localNames, scopeId));
141
+ }
142
+ return scopeClassEntry(value, localNames, scopeId);
143
+ }
144
+ // A token arrives as either the camelCase registry key (`sectionLabel`) or its kebab-case
145
+ // authoring form (`section-label`) — normalize to camelCase FIRST, then decide scoping, so
146
+ // `localNames` (always camelCase, built from the css-parser's registered keys) recognizes a
147
+ // kebab-written token. The emitted (possibly suffixed) name is always the camelCase form.
148
+ function scopeToken(token, localNames, scopeId) {
149
+ const camelToken = kebabToCamel(token);
150
+ return localNames.has(camelToken) ? `${camelToken}__${scopeId}` : camelToken;
151
+ }
152
+ function scopeClassEntry(value, localNames, scopeId) {
153
+ if (typeof value === 'object') {
154
+ const scoped = {};
155
+ for (const [name, enabled] of Object.entries(value)) {
156
+ scoped[scopeToken(name, localNames, scopeId)] = enabled;
157
+ }
158
+ return scoped;
159
+ }
160
+ return value
161
+ .split(/\s+/)
162
+ .filter(Boolean)
163
+ .map(token => scopeToken(token, localNames, scopeId))
164
+ .join(' ');
165
+ }
@@ -0,0 +1,20 @@
1
+ import type { INamedStyles, IViewStyle, ITextStyle } from '../styles';
2
+ type IStyleRecord = Record<string, IViewStyle | ITextStyle>;
3
+ type IStyleObject = Record<string, unknown>;
4
+ export declare function computeHairlineWidth(scale: number): number;
5
+ declare function compose<A, B>(style1: A, style2: B): A | B | [A, B];
6
+ type IStylePreprocessor = (value: unknown) => unknown;
7
+ declare function setStyleAttributePreprocessor(property: string, process: IStylePreprocessor): void;
8
+ declare function flattenWithPreprocessors(style: unknown): Record<string, unknown>;
9
+ declare function roundToNearestPixel(value: number): number;
10
+ export declare const StyleSheet: {
11
+ create<T extends INamedStyles<T> | IStyleRecord>(styles: T & IStyleRecord): T;
12
+ flatten: typeof flattenWithPreprocessors;
13
+ compose: typeof compose;
14
+ setStyleAttributePreprocessor: typeof setStyleAttributePreprocessor;
15
+ roundToNearestPixel: typeof roundToNearestPixel;
16
+ absoluteFill: Readonly<IStyleObject>;
17
+ absoluteFillObject: Readonly<IStyleObject>;
18
+ readonly hairlineWidth: number;
19
+ };
20
+ export {};
@@ -0,0 +1,121 @@
1
+ // StyleSheet, ported from react-native/Libraries/StyleSheet/StyleSheetExports.js.
2
+ // RN's StyleSheet is mostly identity + small helpers: `create` returns the object
3
+ // untouched (no deep-freeze by default in modern RN), `flatten` collapses a style
4
+ // array, `compose` picks/pairs two styles, plus the `hairlineWidth` / `absoluteFill`
5
+ // constants. The typed ViewStyle/TextStyle live next door in styles.ts, so `create`
6
+ // is typed against them like RN's, not left generic over a plain Record.
7
+ import { dlog } from '../debug';
8
+ import { getNativeModule } from '../native-modules';
9
+ import { flattenStyle } from '../style';
10
+ // RN's hairline factor: a "1 physical pixel" line is ~0.4 logical px, rounded to the
11
+ // nearest device pixel. Named so the 0.4 isn't a bare magic number (RN source uses
12
+ // the same literal in PixelRatio.roundToNearestPixel(0.4)).
13
+ const HAIRLINE_LOGICAL_FACTOR = 0.4;
14
+ // Fallback when DeviceInfo isn't resolvable (headless, or before native bring-up).
15
+ // RN can't run without it; we degrade to a sane 1px line rather than crash.
16
+ const HAIRLINE_FALLBACK = 1;
17
+ // Resolve the screen pixel scale lazily from native, or null when unavailable.
18
+ // Lazy (not at import) so this module is importable headless before a fake
19
+ // __turboModuleProxy exists, same precedent as StatusBar's in-effect resolve.
20
+ function resolveScreenScale() {
21
+ const deviceInfo = getNativeModule('DeviceInfo');
22
+ if (deviceInfo === null) {
23
+ dlog('StyleSheet: DeviceInfo not resolvable — hairlineWidth falls back');
24
+ return null;
25
+ }
26
+ const dimensions = deviceInfo.getConstants().Dimensions;
27
+ const scale = dimensions.window?.scale ?? dimensions.windowPhysicalPixels?.scale;
28
+ // A non-positive scale would make the round/divide nonsensical; treat as missing.
29
+ if (typeof scale !== 'number' || scale <= 0) {
30
+ dlog(`StyleSheet: DeviceInfo scale invalid (${String(scale)}) — hairlineWidth falls back`);
31
+ return null;
32
+ }
33
+ return scale;
34
+ }
35
+ // Compute the hairline width for a given scale, mirroring RN exactly: round the
36
+ // logical factor to the nearest device pixel, and if that rounds to 0 (scale < ~1.25)
37
+ // fall back to the thinnest representable line, one physical pixel = 1 / scale.
38
+ // Exported as a pure helper so the smoke can check the formula without faking native.
39
+ export function computeHairlineWidth(scale) {
40
+ const rounded = Math.round(HAIRLINE_LOGICAL_FACTOR * scale) / scale;
41
+ return rounded === 0 ? 1 / scale : rounded;
42
+ }
43
+ // RN freezes absoluteFill in __DEV__; we always freeze so `absoluteFill` and
44
+ // `absoluteFillObject` can safely be the same shared object.
45
+ const absoluteFill = Object.freeze({
46
+ position: 'absolute',
47
+ left: 0,
48
+ right: 0,
49
+ top: 0,
50
+ bottom: 0,
51
+ });
52
+ // RN's composeStyles: falsy left → right, falsy right → left, else the pair [a, b]
53
+ // (which flatten later collapses, later keys winning).
54
+ function compose(style1, style2) {
55
+ if (style1 === null || style1 === undefined)
56
+ return style2;
57
+ if (style2 === null || style2 === undefined)
58
+ return style1;
59
+ return [style1, style2];
60
+ }
61
+ const stylePreprocessors = new Map();
62
+ // Register a value-rewriter for one style property (RN's setStyleAttributePreprocessor,
63
+ // StyleSheetExports.js:151). EXPERIMENTAL in RN; used internally for color/transform.
64
+ // Overwriting an existing preprocessor warns, matching RN's __DEV__ guard.
65
+ function setStyleAttributePreprocessor(property, process) {
66
+ if (stylePreprocessors.has(property)) {
67
+ dlog(`StyleSheet.setStyleAttributePreprocessor: overwriting "${property}" preprocessor`);
68
+ }
69
+ stylePreprocessors.set(property, process);
70
+ }
71
+ // Flatten, then run any registered preprocessor over the matching keys. Kept as a
72
+ // wrapper over the single flattenStyle collapse so the preprocessor map is applied
73
+ // exactly once, at the same seam RN applies it (the style->payload boundary), without
74
+ // reaching into the commit path.
75
+ function flattenWithPreprocessors(style) {
76
+ const flat = flattenStyle(style);
77
+ if (stylePreprocessors.size === 0)
78
+ return flat;
79
+ for (const [property, process] of stylePreprocessors) {
80
+ if (Object.hasOwn(flat, property)) {
81
+ flat[property] = process(flat[property]);
82
+ }
83
+ }
84
+ return flat;
85
+ }
86
+ // Snap a dp size to the nearest value that maps to a whole number of device pixels.
87
+ // RN's StyleSheet.roundToNearestPixel delegates to PixelRatio.roundToNearestPixel
88
+ // (Math.round(size * scale) / scale). PixelRatio lives in the react adapter, which
89
+ // shared cannot import, so the same math runs here over the scale shared already
90
+ // resolves; an unresolvable scale (headless) leaves the value unrounded.
91
+ function roundToNearestPixel(value) {
92
+ const scale = resolveScreenScale();
93
+ if (scale === null)
94
+ return value;
95
+ return Math.round(value * scale) / scale;
96
+ }
97
+ export const StyleSheet = {
98
+ // Identity at runtime, like RN, but the NamedStyles constraint preserves each
99
+ // string-literal style value (flexDirection: 'row' stays 'row', not string) and
100
+ // validates entries, so `styles.box` is assignable to a ViewStyle prop.
101
+ create(styles) {
102
+ return styles;
103
+ },
104
+ // Reuse the single flatten implementation; do not reimplement the clone-on-write
105
+ // collapse here. The wrapper additionally applies any registered per-attribute
106
+ // preprocessor as the style collapses to its flat payload.
107
+ flatten: flattenWithPreprocessors,
108
+ compose,
109
+ setStyleAttributePreprocessor,
110
+ roundToNearestPixel,
111
+ absoluteFill,
112
+ absoluteFillObject: absoluteFill,
113
+ // Lazy compute, recomputed each read (cheap, and avoids caching a fallback taken
114
+ // before native was ready). RN memoizes; we keep it simple until that's a cost.
115
+ get hairlineWidth() {
116
+ const scale = resolveScreenScale();
117
+ if (scale === null)
118
+ return HAIRLINE_FALLBACK;
119
+ return computeHairlineWidth(scale);
120
+ },
121
+ };