@symbiote-native/engine 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/build/accessibility-info/index.android.js +17 -29
  2. package/build/accessibility-info/index.ios.js +23 -37
  3. package/build/accessibility-info/shared.d.ts +3 -2
  4. package/build/accessibility-info/shared.js +25 -2
  5. package/build/action-sheet-ios/index.d.ts +6 -0
  6. package/build/action-sheet-ios/index.js +1 -1
  7. package/build/alert/index.android.js +1 -3
  8. package/build/alert/index.js +1 -1
  9. package/build/animated/animations/base.js +5 -14
  10. package/build/animated/event.js +7 -9
  11. package/build/animated/graph.d.ts +6 -0
  12. package/build/animated/graph.js +14 -0
  13. package/build/animated/interpolation-node.d.ts +0 -1
  14. package/build/animated/interpolation-node.js +5 -4
  15. package/build/animated/native/native-animated.js +1 -3
  16. package/build/animated/operators.d.ts +14 -34
  17. package/build/animated/operators.js +19 -95
  18. package/build/animated/value.d.ts +2 -4
  19. package/build/animated/value.js +17 -5
  20. package/build/app-state/index.js +17 -22
  21. package/build/appearance/index.js +17 -25
  22. package/build/back-handler/index.js +15 -18
  23. package/build/commit.d.ts +1 -3
  24. package/build/commit.js +10 -207
  25. package/build/dimensions/index.js +25 -20
  26. package/build/events/index.js +6 -151
  27. package/build/fabric-props.d.ts +3 -0
  28. package/build/fabric-props.js +186 -0
  29. package/build/i18n-manager/index.js +3 -5
  30. package/build/image-loader.d.ts +24 -0
  31. package/build/image-loader.js +175 -0
  32. package/build/image-source-resolver.d.ts +9 -0
  33. package/build/image-source-resolver.js +16 -0
  34. package/build/index.d.ts +10 -3
  35. package/build/index.js +18 -9
  36. package/build/keyboard/index.js +19 -34
  37. package/build/layout-animation/index.d.ts +1 -0
  38. package/build/layout-animation/index.js +53 -35
  39. package/build/native-modules.d.ts +12 -0
  40. package/build/native-modules.js +64 -1
  41. package/build/node.d.ts +1 -0
  42. package/build/node.js +9 -0
  43. package/build/pan-responder/index.js +1 -3
  44. package/build/permissions-android/index.js +1 -3
  45. package/build/platform-color.d.ts +3 -0
  46. package/build/platform-color.js +24 -0
  47. package/build/process-background-image/index.js +7 -14
  48. package/build/process-box-shadow/index.js +1 -5
  49. package/build/process-filter.js +2 -8
  50. package/build/registry.js +3 -5
  51. package/build/settings/index.js +1 -3
  52. package/build/share/index.ios.js +1 -1
  53. package/build/status-bar/index.android.js +1 -1
  54. package/build/style/index.js +2 -4
  55. package/build/style-registry/index.d.ts +1 -3
  56. package/build/style-registry/index.js +15 -48
  57. package/build/style-registry/scope.d.ts +3 -0
  58. package/build/style-registry/scope.js +41 -0
  59. package/build/touch-history.d.ts +23 -0
  60. package/build/touch-history.js +150 -0
  61. package/build/type-guards.d.ts +4 -0
  62. package/build/type-guards.js +15 -0
  63. package/package.json +2 -2
@@ -1,17 +1,16 @@
1
1
  // Runtime style registry. Side-effect CSS imports (compiled by the sibling CSS-to-style
2
2
  // build package, not this module) call registerStyles() with camelCase keys; components
3
- // look them up by resolveClassName(). No CSS parsing happens here — this is a
4
- // Map<string, ...> lookup, nothing more.
3
+ // look them up via resolveClassName(). No CSS parsing here - just a Map<string, ...> lookup.
5
4
  //
6
- // This registry has no Tailwind-utility detection layer — this repo's style surface has
7
- // no Tailwind layer, so the compound lookup below always runs for 2-4-part class strings
8
- // instead of being gated behind "no part looks like a utility class".
5
+ // No Tailwind-utility detection layer: this repo's style surface has no Tailwind layer,
6
+ // so the compound lookup below always runs for 2-4-part class strings instead of being
7
+ // gated behind "no part looks like a utility class".
9
8
  //
10
9
  // kebab-case authoring: a CSS selector `.section-label` always registers under the
11
10
  // camelCase key `sectionLabel` (@symbiote-native/css-parser's extractClassName), so a template
12
- // can write EITHER `class="sectionLabel"` OR `class="section-label"` — resolveOne below
13
- // falls back to the kebab->camel form on a miss. The fallback matters because assuming
14
- // authors would always match the camelCase key exactly proved wrong in practice.
11
+ // can write EITHER `class="sectionLabel"` OR `class="section-label"` - resolveOne below
12
+ // falls back to the kebab->camel form on a miss, since authors don't reliably write the
13
+ // exact camelCase key.
15
14
  // Compound lookup tries every ordering of 2-4 space-separated class parts joined by
16
15
  // '.' (e.g. "btn primary" -> "btn.primary" / "primary.btn") before falling back to a
17
16
  // per-class merge, mirroring CSS compound-selector registration (`.btn.primary { }`).
@@ -25,14 +24,14 @@ export function registerStyles(styles) {
25
24
  globalStyles.set(name, style);
26
25
  }
27
26
  }
28
- // Clears every registration; used between tests for isolation.
27
+ // Used between tests to reset registry state.
29
28
  export function clearGlobalStyles() {
30
29
  globalStyles.clear();
31
30
  }
32
31
  // A `class`/`className` prop arrives as `unknown` at the routeProp boundary (any adapter can
33
32
  // hand over anything); this narrows before resolveClassName without an `as` cast. Shared with
34
33
  // adapters/vue/src/components/scroll-view/shared.ts's identical need, rather than each keeping
35
- // its own copy — that file imports this one instead of redeclaring it.
34
+ // its own copy - that file imports this one instead of redeclaring it.
36
35
  export function isClassNameValue(value) {
37
36
  return typeof value === 'string' || (typeof value === 'object' && value !== null);
38
37
  }
@@ -101,10 +100,14 @@ function capitalize(value) {
101
100
  }
102
101
  // Duplicated from @symbiote-native/css-parser's identical helper rather than imported: css-parser
103
102
  // pulls in postcss and is build-time only (never shipped in the app bundle), and this registry
104
- // is the opposite — pure runtime, in every app bundle — so importing it here would leak a
103
+ // is the opposite - pure runtime, in every app bundle - so importing it here would leak a
105
104
  // build-time dependency into the shipped app. The conversion itself is two lines; keeping both
106
105
  // copies in sync is a smaller cost than the alternative.
107
- function kebabToCamel(value) {
106
+ //
107
+ // Exported (not just local to this file) because ./scope.ts's Vue `<style scoped>` name
108
+ // rewriter needs the same kebab->camel normalization and is a different responsibility living
109
+ // in a sibling module - see that file's own doc comment for why it's split out of this one.
110
+ export function kebabToCamel(value) {
108
111
  return value.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
109
112
  }
110
113
  function tryCompoundLookup(parts) {
@@ -123,39 +126,3 @@ function resolveOne(name) {
123
126
  return {};
124
127
  return globalStyles.get(trimmed) ?? globalStyles.get(kebabToCamel(trimmed)) ?? {};
125
128
  }
126
- // Pure NAME rewriter for Vue `<style scoped>`: suffixes every class token that this
127
- // file's scoped block locally defines with `__${scopeId}`, leaving unrecognized tokens
128
- // (globals, external classes) untouched. Runs at the compiled call site BEFORE Vue's own
129
- // normalizeClass() collapses string/object/array `class` values to a final string, so it
130
- // must pre-process all three shapes normalizeClass understands. No registry lookup here —
131
- // resolveClassName still does the actual style lookup, unchanged, against the rewritten name.
132
- export function scopeClassName(value, localNames, scopeId) {
133
- if (value === undefined || value === null)
134
- return value;
135
- if (Array.isArray(value)) {
136
- return value.map(item => scopeClassEntry(item, localNames, scopeId));
137
- }
138
- return scopeClassEntry(value, localNames, scopeId);
139
- }
140
- // A token arrives as either the camelCase registry key (`sectionLabel`) or its kebab-case
141
- // authoring form (`section-label`) — normalize to camelCase FIRST, then decide scoping, so
142
- // `localNames` (always camelCase, built from the css-parser's registered keys) recognizes a
143
- // kebab-written token. The emitted (possibly suffixed) name is always the camelCase form.
144
- function scopeToken(token, localNames, scopeId) {
145
- const camelToken = kebabToCamel(token);
146
- return localNames.has(camelToken) ? `${camelToken}__${scopeId}` : camelToken;
147
- }
148
- function scopeClassEntry(value, localNames, scopeId) {
149
- if (typeof value === 'object') {
150
- const scoped = {};
151
- for (const [name, enabled] of Object.entries(value)) {
152
- scoped[scopeToken(name, localNames, scopeId)] = enabled;
153
- }
154
- return scoped;
155
- }
156
- return value
157
- .split(/\s+/)
158
- .filter(Boolean)
159
- .map(token => scopeToken(token, localNames, scopeId))
160
- .join(' ');
161
- }
@@ -0,0 +1,3 @@
1
+ export type IClassToggleMap = Record<string, boolean | undefined>;
2
+ export type IScopableClassValue = string | IClassToggleMap | Array<string | IClassToggleMap> | undefined | null;
3
+ export declare function scopeClassName(value: IScopableClassValue, localNames: ReadonlySet<string>, scopeId: string): IScopableClassValue;
@@ -0,0 +1,41 @@
1
+ // Vue `<style scoped>` class-name rewriter. Distinct responsibility from the sibling
2
+ // ./index.ts (the CSS class -> style registry): this module does pure NAME rewriting,
3
+ // no registry lookup, no CSS parsing. It runs at the compiled call site of a Vue SFC's
4
+ // scoped-style template - `adapters/vue/metro-vue-transformer.cjs` emits calls to
5
+ // scopeClassName (imported there as `__scopeClass`) - BEFORE Vue's own normalizeClass()
6
+ // collapses string/object/array `class` values to a final string, so it must pre-process
7
+ // all three shapes normalizeClass understands. resolveClassName in ./index.ts still does
8
+ // the actual style lookup, unchanged, against the rewritten (possibly suffixed) name.
9
+ import { kebabToCamel } from './index.js';
10
+ // Suffixes every class token that this file's scoped block locally defines with
11
+ // `__${scopeId}`, leaving unrecognized tokens (globals, external classes) untouched.
12
+ export function scopeClassName(value, localNames, scopeId) {
13
+ if (value === undefined || value === null)
14
+ return value;
15
+ if (Array.isArray(value)) {
16
+ return value.map(item => scopeClassEntry(item, localNames, scopeId));
17
+ }
18
+ return scopeClassEntry(value, localNames, scopeId);
19
+ }
20
+ // A token arrives as either the camelCase registry key (`sectionLabel`) or its kebab-case
21
+ // authoring form (`section-label`) - normalize to camelCase FIRST, then decide scoping, so
22
+ // `localNames` (always camelCase, built from the css-parser's registered keys) recognizes a
23
+ // kebab-written token. The emitted (possibly suffixed) name is always the camelCase form.
24
+ function scopeToken(token, localNames, scopeId) {
25
+ const camelToken = kebabToCamel(token);
26
+ return localNames.has(camelToken) ? `${camelToken}__${scopeId}` : camelToken;
27
+ }
28
+ function scopeClassEntry(value, localNames, scopeId) {
29
+ if (typeof value === 'object') {
30
+ const scoped = {};
31
+ for (const [name, enabled] of Object.entries(value)) {
32
+ scoped[scopeToken(name, localNames, scopeId)] = enabled;
33
+ }
34
+ return scoped;
35
+ }
36
+ return value
37
+ .split(/\s+/)
38
+ .filter(Boolean)
39
+ .map(token => scopeToken(token, localNames, scopeId))
40
+ .join(' ');
41
+ }
@@ -0,0 +1,23 @@
1
+ interface ITouchRecord {
2
+ touchActive: boolean;
3
+ startPageX: number;
4
+ startPageY: number;
5
+ startTimeStamp: number;
6
+ currentPageX: number;
7
+ currentPageY: number;
8
+ currentTimeStamp: number;
9
+ previousPageX: number;
10
+ previousPageY: number;
11
+ previousTimeStamp: number;
12
+ }
13
+ interface ITouchHistory {
14
+ touchBank: ITouchRecord[];
15
+ numberActiveTouches: number;
16
+ indexOfSingleActiveTouch: number;
17
+ mostRecentTimeStamp: number;
18
+ }
19
+ export declare const touchHistory: ITouchHistory;
20
+ export declare function recordTouchTrack(kind: 'start' | 'move' | 'end', nativeEvent: Record<string, unknown>): void;
21
+ export declare function resetTouchHistory(): void;
22
+ export declare function attachTouchHistory(nativeEvent: Record<string, unknown>): void;
23
+ export {};
@@ -0,0 +1,150 @@
1
+ // Per-touch position/time tracking, ported from RN's
2
+ // react-native-renderer/.../legacy-events/ResponderTouchHistoryStore.js. PanResponder's
3
+ // multitouch dx/vx math needs each touch's own previous->current delta (RN counts only
4
+ // touches that moved since `_accountsForMovesUpTo`), which a grant-relative centroid of
5
+ // ALL live touches cannot reconstruct. We maintain the bank as touches flow and ATTACH
6
+ // `touchHistory` onto the nativeEvent reaching responder handlers, exactly how
7
+ // ResponderEventPlugin.js sets `*.touchHistory`.
8
+ //
9
+ // events/index.ts consumes only this file's public surface (recordTouchTrack,
10
+ // attachTouchHistory, resetTouchHistory, touchHistory); everything else here is a
11
+ // private implementation detail of the bank.
12
+ import { isRecord } from './type-guards.js';
13
+ // RN's bank is indexed by touch identifier and warns above 20; we never warn (headless
14
+ // events may carry larger or absent ids), we just skip anything out of a sane range.
15
+ const MAX_TOUCH_BANK = 20;
16
+ const touchBank = [];
17
+ export const touchHistory = {
18
+ touchBank,
19
+ numberActiveTouches: 0,
20
+ indexOfSingleActiveTouch: -1,
21
+ mostRecentTimeStamp: 0,
22
+ };
23
+ function toFiniteNumber(value) {
24
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
25
+ }
26
+ // Pull a recordable touch out of an untyped entry. RN's getTouchIdentifier throws on
27
+ // a null id; we skip instead, so events without touch geometry leave the bank untouched.
28
+ function normalizeTouch(raw) {
29
+ if (!isRecord(raw))
30
+ return undefined;
31
+ const identifier = toFiniteNumber(raw.identifier);
32
+ const pageX = toFiniteNumber(raw.pageX);
33
+ const pageY = toFiniteNumber(raw.pageY);
34
+ if (identifier === undefined || pageX === undefined || pageY === undefined)
35
+ return undefined;
36
+ if (identifier < 0 || identifier > MAX_TOUCH_BANK)
37
+ return undefined;
38
+ return { identifier, pageX, pageY, timestamp: toFiniteNumber(raw.timestamp) ?? 0 };
39
+ }
40
+ // The changed touches for this frame (start/move/end), defensively read.
41
+ function changedTouchesOf(nativeEvent) {
42
+ const raw = nativeEvent.changedTouches;
43
+ if (!Array.isArray(raw))
44
+ return [];
45
+ const out = [];
46
+ for (const entry of raw) {
47
+ const touch = normalizeTouch(entry);
48
+ if (touch !== undefined)
49
+ out.push(touch);
50
+ }
51
+ return out;
52
+ }
53
+ // Count of all touches still down (RN reads nativeEvent.touches.length directly).
54
+ function activeTouchCount(nativeEvent) {
55
+ const raw = nativeEvent.touches;
56
+ return Array.isArray(raw) ? raw.length : 0;
57
+ }
58
+ function recordTouchStart(touch) {
59
+ const record = touchBank[touch.identifier];
60
+ if (record) {
61
+ record.touchActive = true;
62
+ record.startPageX = touch.pageX;
63
+ record.startPageY = touch.pageY;
64
+ record.startTimeStamp = touch.timestamp;
65
+ record.currentPageX = touch.pageX;
66
+ record.currentPageY = touch.pageY;
67
+ record.currentTimeStamp = touch.timestamp;
68
+ record.previousPageX = touch.pageX;
69
+ record.previousPageY = touch.pageY;
70
+ record.previousTimeStamp = touch.timestamp;
71
+ }
72
+ else {
73
+ touchBank[touch.identifier] = {
74
+ touchActive: true,
75
+ startPageX: touch.pageX,
76
+ startPageY: touch.pageY,
77
+ startTimeStamp: touch.timestamp,
78
+ currentPageX: touch.pageX,
79
+ currentPageY: touch.pageY,
80
+ currentTimeStamp: touch.timestamp,
81
+ previousPageX: touch.pageX,
82
+ previousPageY: touch.pageY,
83
+ previousTimeStamp: touch.timestamp,
84
+ };
85
+ }
86
+ touchHistory.mostRecentTimeStamp = touch.timestamp;
87
+ }
88
+ // Move and end share the previous<-current shift; only `touchActive` differs.
89
+ function shiftTouchRecord(touch, active) {
90
+ const record = touchBank[touch.identifier];
91
+ if (!record)
92
+ return;
93
+ record.touchActive = active;
94
+ record.previousPageX = record.currentPageX;
95
+ record.previousPageY = record.currentPageY;
96
+ record.previousTimeStamp = record.currentTimeStamp;
97
+ record.currentPageX = touch.pageX;
98
+ record.currentPageY = touch.pageY;
99
+ record.currentTimeStamp = touch.timestamp;
100
+ touchHistory.mostRecentTimeStamp = touch.timestamp;
101
+ }
102
+ function arrayFirst(value) {
103
+ return Array.isArray(value) ? value[0] : undefined;
104
+ }
105
+ // Maintain the bank as a touch frame flows. Mirrors RN's recordTouchTrack: moveish
106
+ // shifts records, startish records + recomputes numberActiveTouches, endish marks the
107
+ // record inactive + rescans for the single remaining touch. `kind` is the touch phase.
108
+ export function recordTouchTrack(kind, nativeEvent) {
109
+ if (kind === 'move') {
110
+ for (const touch of changedTouchesOf(nativeEvent))
111
+ shiftTouchRecord(touch, true);
112
+ return;
113
+ }
114
+ if (kind === 'start') {
115
+ for (const touch of changedTouchesOf(nativeEvent))
116
+ recordTouchStart(touch);
117
+ touchHistory.numberActiveTouches = activeTouchCount(nativeEvent);
118
+ if (touchHistory.numberActiveTouches === 1) {
119
+ const first = normalizeTouch(arrayFirst(nativeEvent.touches));
120
+ touchHistory.indexOfSingleActiveTouch = first?.identifier ?? -1;
121
+ }
122
+ return;
123
+ }
124
+ for (const touch of changedTouchesOf(nativeEvent))
125
+ shiftTouchRecord(touch, false);
126
+ touchHistory.numberActiveTouches = activeTouchCount(nativeEvent);
127
+ if (touchHistory.numberActiveTouches === 1) {
128
+ for (let i = 0; i < touchBank.length; i++) {
129
+ const record = touchBank[i];
130
+ if (record !== undefined && record.touchActive) {
131
+ touchHistory.indexOfSingleActiveTouch = i;
132
+ break;
133
+ }
134
+ }
135
+ }
136
+ }
137
+ // Drop all touch state. Called on a fully-released / cancelled gesture so a stale bank
138
+ // never leaks geometry into the next gesture's first frame.
139
+ export function resetTouchHistory() {
140
+ touchBank.length = 0;
141
+ touchHistory.numberActiveTouches = 0;
142
+ touchHistory.indexOfSingleActiveTouch = -1;
143
+ touchHistory.mostRecentTimeStamp = 0;
144
+ }
145
+ // Attach the live touch history onto the event the responder handlers receive, matching
146
+ // ResponderEventPlugin.js (`grantEvent.touchHistory = ...`, etc.). PanResponder reads
147
+ // it for the per-touch dx/vx math; handlers that ignore it are unaffected.
148
+ export function attachTouchHistory(nativeEvent) {
149
+ nativeEvent.touchHistory = touchHistory;
150
+ }
@@ -0,0 +1,4 @@
1
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
2
+ export declare function isBoolean(value: unknown): value is boolean;
3
+ export declare function isNumber(value: unknown): value is number;
4
+ export declare function isString(value: unknown): value is string;
@@ -0,0 +1,15 @@
1
+ // Runtime guards to narrow `unknown` at trust boundaries (native payloads, ViewConfig
2
+ // attributes, style values) without an `as` cast. `isRecord` excludes arrays - most
3
+ // call sites mean "a native payload keyed by string," never a list.
4
+ export function isRecord(value) {
5
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
6
+ }
7
+ export function isBoolean(value) {
8
+ return typeof value === 'boolean';
9
+ }
10
+ export function isNumber(value) {
11
+ return typeof value === 'number';
12
+ }
13
+ export function isString(value) {
14
+ return typeof value === 'string';
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/engine",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "SymbioteNative's retained shadow-tree engine — clone-on-write commit path + event normalization over React Native Fabric, shared by every framework adapter.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,7 +32,7 @@
32
32
  "react-native": ">=0.86"
33
33
  },
34
34
  "devDependencies": {
35
- "@symbiote-native/test-utils": "0.1.3"
35
+ "@symbiote-native/test-utils": "0.1.5"
36
36
  },
37
37
  "scripts": {
38
38
  "typecheck": "tsc --build",