@symbiote-native/components 1.0.0 → 2.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.
- package/README.md +3 -4
- package/build/behaviors/activity-indicator/index.android.d.ts +1 -0
- package/build/behaviors/activity-indicator/index.android.js +16 -0
- package/build/behaviors/activity-indicator/index.d.ts +3 -0
- package/build/behaviors/activity-indicator/index.ios.d.ts +1 -0
- package/build/behaviors/activity-indicator/index.ios.js +14 -0
- package/build/behaviors/activity-indicator/index.js +5 -0
- package/build/behaviors/activity-indicator/shared.d.ts +18 -0
- package/build/behaviors/activity-indicator/shared.js +149 -0
- package/build/behaviors/button.d.ts +2 -0
- package/build/behaviors/button.js +328 -0
- package/build/behaviors/image-background.d.ts +2 -0
- package/build/behaviors/image-background.js +139 -0
- package/build/behaviors/image.d.ts +1 -1
- package/build/behaviors/image.js +2 -2
- package/build/behaviors/input-accessory-view.d.ts +1 -1
- package/build/behaviors/input-accessory-view.js +2 -2
- package/build/behaviors/pressable.d.ts +59 -1
- package/build/behaviors/pressable.js +93 -18
- package/build/behaviors/refresh-control.d.ts +2 -0
- package/build/behaviors/refresh-control.js +83 -0
- package/build/behaviors/scroll-view/index.android.d.ts +1 -0
- package/build/behaviors/scroll-view/index.android.js +52 -0
- package/build/behaviors/scroll-view/index.d.ts +2 -0
- package/build/behaviors/scroll-view/index.ios.d.ts +1 -0
- package/build/behaviors/scroll-view/index.ios.js +10 -0
- package/build/behaviors/scroll-view/index.js +5 -0
- package/build/behaviors/scroll-view/shared.d.ts +11 -0
- package/build/behaviors/scroll-view/shared.js +291 -0
- package/build/behaviors/scroll-view/sticky.d.ts +17 -0
- package/build/behaviors/scroll-view/sticky.js +568 -0
- package/build/behaviors/switch.d.ts +1 -1
- package/build/behaviors/switch.js +9 -5
- package/build/behaviors/text-input.d.ts +2 -2
- package/build/behaviors/text-input.js +43 -15
- package/build/behaviors/touchable-highlight.d.ts +9 -0
- package/build/behaviors/touchable-highlight.js +192 -0
- package/build/behaviors/touchable-native-feedback.d.ts +20 -0
- package/build/behaviors/touchable-native-feedback.js +333 -0
- package/build/behaviors/touchable-opacity.d.ts +12 -0
- package/build/behaviors/touchable-opacity.js +227 -0
- package/build/behaviors/touchable-without-feedback.d.ts +2 -0
- package/build/behaviors/touchable-without-feedback.js +296 -0
- package/build/component-names/index.android.js +29 -19
- package/build/component-names/index.ios.js +31 -19
- package/build/component-names/shared.d.ts +2 -1
- package/build/component-names/shared.js +16 -6
- package/build/descriptor.js +4 -4
- package/build/fold-host-bag.js +2 -2
- package/build/index.d.ts +17 -11
- package/build/index.js +44 -8
- package/build/register.d.ts +1 -0
- package/build/register.js +55 -0
- package/build/scroll-view-commands.d.ts +4 -0
- package/build/scroll-view-commands.js +30 -31
- package/build/state/text-input.d.ts +4 -1
- package/build/view/render-button.d.ts +36 -1
- package/build/view/render-button.js +101 -12
- package/build/view/render-image/index.js +2 -2
- package/build/view/render-input-accessory-view.js +1 -1
- package/build/view/render-modal.js +3 -3
- package/build/view/render-pressable/index.d.ts +2 -0
- package/build/view/render-pressable/index.js +24 -0
- package/build/view/render-scroll-view.d.ts +1 -0
- package/build/view/render-scroll-view.js +18 -9
- package/build/view/render-switch.d.ts +4 -1
- package/build/view/render-switch.js +2 -2
- package/build/view/render-text-input.js +2 -2
- package/build/view/render-touchable-native-feedback.d.ts +19 -0
- package/build/view/render-touchable-native-feedback.js +19 -0
- package/host-primitives.cjs +202 -150
- package/host-primitives.d.cts +0 -2
- package/package.json +8 -17
- package/build/state-style.d.ts +0 -15
- package/build/state-style.js +0 -47
- package/build/view/render-activity-indicator.d.ts +0 -25
- package/build/view/render-activity-indicator.js +0 -88
- package/build/view/render-image-background.d.ts +0 -9
- package/build/view/render-image-background.js +0 -48
- package/lowering-fixtures.cjs +0 -259
- package/lowering-fixtures.d.cts +0 -17
- package/specialize-state-style.cjs +0 -219
- package/specialize-state-style.d.cts +0 -15
package/build/state-style.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
// The runtime half of state-style lowering: splits an authored `style` into its resting and pressed
|
|
2
|
-
// halves. Referenced by the code every lowering transform EMITS, never by app code.
|
|
3
|
-
//
|
|
4
|
-
// SHARED for the same reason `HOST_PRIMITIVES` and `specializeStateStyle` are — two adapters had
|
|
5
|
-
// written byte-identical copies within an hour of each other, which is the duplication
|
|
6
|
-
// `<adapters_stay_thin>` exists to stop. Each adapter re-exports it from its own `./state-style`
|
|
7
|
-
// subpath so the emitted import specifier stays inside the package the app already depends on.
|
|
8
|
-
//
|
|
9
|
-
// WHY A HELPER EXISTS BESIDE THE INLINE GUARD, since a transform may emit either. Building the pair
|
|
10
|
-
// needs the authored `style` expression, and the two emissions are NOT equivalent:
|
|
11
|
-
//
|
|
12
|
-
// inline typeof e === 'function' ? e({pressed:false}) : e prints `e` on both props, so a
|
|
13
|
-
// `getStyle()` runs twice and a `flag ? a : b` can take DIFFERENT branches
|
|
14
|
-
// helper resolveStateStyle(e) prints `e` once and calls its
|
|
15
|
-
// RESULT twice
|
|
16
|
-
//
|
|
17
|
-
// The rule the two must satisfy is `REFUSAL_CATEGORIES.emitStyleExpressionOnce`, and it is about
|
|
18
|
-
// the output rather than the input: **an expression capable of DOING WORK is printed once.** A bare
|
|
19
|
-
// name or a non-computed dotted path is a read, not work, so the inline form is legitimate there —
|
|
20
|
-
// and it matters, because a helper returns one object, which a framework must spread, and a spread
|
|
21
|
-
// costs the element its patch flag (Vue measured `12 /* STYLE, PROPS */` becoming
|
|
22
|
-
// `16 /* FULL_PROPS */` plus a `mergeProps` per render on the hottest element in the tree).
|
|
23
|
-
//
|
|
24
|
-
// So the VERDICT is identical across adapters — every shape lowers — and only the cost differs,
|
|
25
|
-
// exactly as compile-time substitution differs between a JSX path and an SFC one. The verdicts are
|
|
26
|
-
// what `@symbiote-native/components/lowering-fixtures` pins.
|
|
27
|
-
//
|
|
28
|
-
// THE CONTRACT THIS IMPOSES ON APP CODE: a style callback must be PURE in `pressed`. Its result is
|
|
29
|
-
// invoked twice under every emission, so a side-effecting body is observable now.
|
|
30
|
-
function isStyleCallback(value) {
|
|
31
|
-
return typeof value === 'function';
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Splits an authored `style` into its resting and pressed halves, reading the value once.
|
|
35
|
-
*
|
|
36
|
-
* A non-callback passes through untouched with no active variant, which the engine reads as "leave
|
|
37
|
-
* slot 1 alone" — that is what makes one emission safe for an expression whose value cannot be
|
|
38
|
-
* known at compile time.
|
|
39
|
-
*/
|
|
40
|
-
export function resolveStateStyle(value) {
|
|
41
|
-
if (!isStyleCallback(value))
|
|
42
|
-
return { style: value, activeStyle: undefined };
|
|
43
|
-
return {
|
|
44
|
-
style: value({ pressed: false }),
|
|
45
|
-
activeStyle: value({ pressed: true }),
|
|
46
|
-
};
|
|
47
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { IStyleProp, IViewStyle, ISymbioteEvent } from '@symbiote-native/engine';
|
|
2
|
-
import type { IDescriptor } from '../descriptor';
|
|
3
|
-
import type { IAccessibilityProps, IAriaProps } from '../accessibility-props';
|
|
4
|
-
export type IActivityIndicatorSize = 'small' | 'large' | number;
|
|
5
|
-
export interface IActivityIndicatorProps extends IAccessibilityProps, IAriaProps {
|
|
6
|
-
animating?: boolean;
|
|
7
|
-
color?: string;
|
|
8
|
-
size?: IActivityIndicatorSize;
|
|
9
|
-
hidesWhenStopped?: boolean;
|
|
10
|
-
style?: IStyleProp<IViewStyle>;
|
|
11
|
-
onLayout?: (event: ISymbioteEvent) => void;
|
|
12
|
-
}
|
|
13
|
-
export type IActivityIndicatorViewProps = {
|
|
14
|
-
animating: boolean;
|
|
15
|
-
hidesWhenStopped: boolean;
|
|
16
|
-
size: IActivityIndicatorSize;
|
|
17
|
-
color?: string;
|
|
18
|
-
style?: IStyleProp<IViewStyle>;
|
|
19
|
-
passthrough: Record<string, unknown>;
|
|
20
|
-
};
|
|
21
|
-
export type IActivityIndicatorPlatform = {
|
|
22
|
-
defaultColor: string | null;
|
|
23
|
-
nativeExtras: Readonly<Record<string, unknown>>;
|
|
24
|
-
};
|
|
25
|
-
export declare function renderActivityIndicator(view: IActivityIndicatorViewProps, platform: IActivityIndicatorPlatform): IDescriptor;
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
// ACTIVITYINDICATOR IS NOT A LOWERABLE PRIMITIVE, AND WILL NOT BECOME ONE. Decided 2026-09-01.
|
|
2
|
-
// It stays a component in every adapter; do not add it to `HOST_PRIMITIVES`.
|
|
3
|
-
//
|
|
4
|
-
// The reason is the `el('symbiote-view', wrapperProps, [el('symbiote-activity-indicator', …)])` at
|
|
5
|
-
// the bottom of this file: the render SYNTHESIZES a node that is not the primitive itself. A lowered
|
|
6
|
-
// tag is ONE engine node, and a host behavior's `foldPayload` maps props to props on that node — it
|
|
7
|
-
// cannot create a child. Lowering would therefore drop the centering container and change layout,
|
|
8
|
-
// which is an optimisation moving the observable surface, the one thing this design may not do.
|
|
9
|
-
//
|
|
10
|
-
// The wrapper is not over-building: RN's own `ActivityIndicator.js:112` renders a `<View>` around
|
|
11
|
-
// the native spinner too, so the two nodes are inherent. And the container cannot be folded INTO the
|
|
12
|
-
// spinner — it carries `alignItems`/`justifyContent`, which centre the spinner inside the space it
|
|
13
|
-
// was given; moved onto the spinner they would centre its children, of which it has none.
|
|
14
|
-
//
|
|
15
|
-
// The two alternatives were priced and both cost more than the primitive is worth. A behavior that
|
|
16
|
-
// CREATES a node needs a commit hook, i.e. a machine, i.e. a `-managed` tag split — a new category,
|
|
17
|
-
// not a fold. Synthesising the container inside the engine's commit walk is NOT the `RCTVirtualText`
|
|
18
|
-
// precedent it resembles: `viewNameFor` changes what one node IS and never ADDS one, so adding one
|
|
19
|
-
// would make the retained and committed trees disagree on node count — which every counter, census
|
|
20
|
-
// probe and benchmark in this repo assumes. Spinners are rare on a screen, so the per-instance cost
|
|
21
|
-
// lowering removes is not measurably paid here; the objection to leaving it is uniformity, not speed.
|
|
22
|
-
//
|
|
23
|
-
// Under half A this costs an app NOTHING: `View` and `ActivityIndicator` are both ordinary imports
|
|
24
|
-
// from the adapter barrel, so which one is internally a tag and which a component is invisible at
|
|
25
|
-
// the call site. The general rule is in `.claude/rules/host-primitive-tier.md`.
|
|
26
|
-
//
|
|
27
|
-
// ActivityIndicator: the render half (framework-agnostic). RN wraps the native spinner
|
|
28
|
-
// in a centering View and translates `size` in JS: 'small'/'large' map to a native size
|
|
29
|
-
// enum AND a fixed box style; a numeric size never reaches native (it sizes the spinner
|
|
30
|
-
// via style only). That translation is platform-invariant and lives here.
|
|
31
|
-
//
|
|
32
|
-
// What IS platform-specific: Android's AndroidProgressBar needs
|
|
33
|
-
// `styleAttr` (which triggers its setStyle(), without it the view throws "setStyle() not
|
|
34
|
-
// called") plus `indeterminate: true`, and its default color is the theme (null), whereas
|
|
35
|
-
// iOS's ActivityIndicatorView takes neither and defaults to GRAY. The adapter's per-host
|
|
36
|
-
// file supplies those bits via `platform`.
|
|
37
|
-
import { dlog } from '@symbiote-native/engine';
|
|
38
|
-
import { el } from '../descriptor.js';
|
|
39
|
-
// Fixed pixel boxes RN gives the two named sizes (styles.sizeSmall/sizeLarge).
|
|
40
|
-
const SIZE_SMALL_PX = 20;
|
|
41
|
-
const SIZE_LARGE_PX = 36;
|
|
42
|
-
// Centering wrapper RN puts around the spinner (styles.container).
|
|
43
|
-
const CONTAINER_STYLE = {
|
|
44
|
-
alignItems: 'center',
|
|
45
|
-
justifyContent: 'center',
|
|
46
|
-
};
|
|
47
|
-
function resolveSize(size) {
|
|
48
|
-
if (size === 'small') {
|
|
49
|
-
return {
|
|
50
|
-
sizeStyle: { width: SIZE_SMALL_PX, height: SIZE_SMALL_PX },
|
|
51
|
-
sizeProp: 'small',
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
if (size === 'large') {
|
|
55
|
-
return {
|
|
56
|
-
sizeStyle: { width: SIZE_LARGE_PX, height: SIZE_LARGE_PX },
|
|
57
|
-
sizeProp: 'large',
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
return { sizeStyle: { width: size, height: size } };
|
|
61
|
-
}
|
|
62
|
-
export function renderActivityIndicator(view, platform) {
|
|
63
|
-
const { sizeStyle, sizeProp } = resolveSize(view.size);
|
|
64
|
-
dlog(sizeProp !== undefined
|
|
65
|
-
? `ActivityIndicator size '${sizeProp}' -> native size enum '${sizeProp}'`
|
|
66
|
-
: `ActivityIndicator size ${String(view.size)} -> style only, native size not set`);
|
|
67
|
-
const nativeProps = {
|
|
68
|
-
animating: view.animating,
|
|
69
|
-
hidesWhenStopped: view.hidesWhenStopped,
|
|
70
|
-
style: sizeStyle,
|
|
71
|
-
...platform.nativeExtras,
|
|
72
|
-
};
|
|
73
|
-
// Omit color entirely when neither given nor defaulted (Android's theme default is
|
|
74
|
-
// null); a null color prop would be rejected by Fabric's color parser.
|
|
75
|
-
const resolvedColor = view.color ?? platform.defaultColor;
|
|
76
|
-
if (resolvedColor !== null)
|
|
77
|
-
nativeProps.color = resolvedColor;
|
|
78
|
-
if (sizeProp !== undefined)
|
|
79
|
-
nativeProps.size = sizeProp;
|
|
80
|
-
dlog('ActivityIndicator -> RCTView(spinner)');
|
|
81
|
-
const wrapperProps = {
|
|
82
|
-
...view.passthrough,
|
|
83
|
-
style: [CONTAINER_STYLE, view.style],
|
|
84
|
-
};
|
|
85
|
-
return el('symbiote-view', wrapperProps, [
|
|
86
|
-
el('symbiote-activity-indicator', nativeProps),
|
|
87
|
-
]);
|
|
88
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { type IStyleProp, type IViewStyle } from '@symbiote-native/engine';
|
|
2
|
-
import { type IDescriptor } from '../descriptor';
|
|
3
|
-
import { type IImageViewProps } from './render-image';
|
|
4
|
-
export type IImageBackgroundViewProps = {
|
|
5
|
-
style?: IStyleProp<IViewStyle>;
|
|
6
|
-
imageStyle?: IStyleProp<IViewStyle>;
|
|
7
|
-
image: IImageViewProps;
|
|
8
|
-
};
|
|
9
|
-
export declare function renderImageBackground(view: IImageBackgroundViewProps): IDescriptor;
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
// ImageBackground: the render half (framework-agnostic). Pure JS composition, no native
|
|
2
|
-
// component of its own (mirrors react-native/Libraries/Image/ImageBackground.js): an outer
|
|
3
|
-
// View receives the wrapper `style`; an absolutely-filled Image sits behind it; the user's
|
|
4
|
-
// `children` paint on top (as siblings AFTER the image in the wrapper's child order, injected
|
|
5
|
-
// by the adapter). Shared verbatim across adapters: React and Vue both bridge this Descriptor.
|
|
6
|
-
//
|
|
7
|
-
// The inner Image is positioned absolute-fill and has the wrapper's width/height reapplied:
|
|
8
|
-
// RN's Image overwrites its own width/height from the source's intrinsic size, which would
|
|
9
|
-
// fight the wrapper's explicit dimensions, so we proxy them back onto the Image so it fills
|
|
10
|
-
// the box. `imageStyle` wins last.
|
|
11
|
-
import { dlog, flattenStyle, } from '@symbiote-native/engine';
|
|
12
|
-
import { el } from '../descriptor.js';
|
|
13
|
-
import { renderImage } from './render-image/index.js';
|
|
14
|
-
// The inner Image's positioning: absolute-fill behind the wrapper's children.
|
|
15
|
-
const IMAGE_BACKGROUND_ABSOLUTE_FILL = {
|
|
16
|
-
position: 'absolute',
|
|
17
|
-
left: 0,
|
|
18
|
-
right: 0,
|
|
19
|
-
top: 0,
|
|
20
|
-
bottom: 0,
|
|
21
|
-
};
|
|
22
|
-
// Read one explicit dimension off the (already-flattened) wrapper style. A dp number or a
|
|
23
|
-
// percentage string is a valid IDimensionValue; anything else (auto / undefined) yields undefined.
|
|
24
|
-
function readDimension(style, key) {
|
|
25
|
-
const value = Object.hasOwn(style, key) ? Reflect.get(style, key) : undefined;
|
|
26
|
-
if (typeof value === 'number' || typeof value === 'string')
|
|
27
|
-
return value;
|
|
28
|
-
return undefined;
|
|
29
|
-
}
|
|
30
|
-
export function renderImageBackground(view) {
|
|
31
|
-
// Flatten only to read the wrapper's explicit dimensions; RN copies these onto the Image so
|
|
32
|
-
// it fills the box rather than collapsing to the source's intrinsic size. `imageStyle` last.
|
|
33
|
-
const flattenedWrapper = flattenStyle(view.style);
|
|
34
|
-
const imageMergedStyle = [
|
|
35
|
-
IMAGE_BACKGROUND_ABSOLUTE_FILL,
|
|
36
|
-
{
|
|
37
|
-
width: readDimension(flattenedWrapper, 'width'),
|
|
38
|
-
height: readDimension(flattenedWrapper, 'height'),
|
|
39
|
-
},
|
|
40
|
-
view.imageStyle,
|
|
41
|
-
];
|
|
42
|
-
dlog('ImageBackground -> View(RCTView) > Image(RCTImageView absolute-fill) + children');
|
|
43
|
-
// The wrapper View holds the inner Image as its only structural child; the adapter appends
|
|
44
|
-
// the user children after it (so they paint on top).
|
|
45
|
-
return el('symbiote-view', { style: view.style }, [
|
|
46
|
-
renderImage({ ...view.image, style: imageMergedStyle }),
|
|
47
|
-
]);
|
|
48
|
-
}
|
package/lowering-fixtures.cjs
DELETED
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
// The shared VERDICT table for host-primitive lowering — the fifth parity surface named in
|
|
2
|
-
// `.claude/rules/adapter-parity-audit.md`.
|
|
3
|
-
//
|
|
4
|
-
// WHY A TABLE OF CASES AND NOT SHARED CODE. Four transforms implement one rule set —
|
|
5
|
-
// `adapters/solid/babel-lower-host-primitives.cjs`, `adapters/vue/babel-lower-host-primitives.cjs`,
|
|
6
|
-
// `adapters/vue/metro-vue-transformer.cjs`, `adapters/svelte/src/preprocessor/lower-host-
|
|
7
|
-
// primitives.ts` — over three different plumbings: a Babel plugin holding a real AST, an SFC
|
|
8
|
-
// transform handed SOURCE TEXT, and a Svelte preprocessor that reads ESTree and emits text. They
|
|
9
|
-
// share a spec (`host-primitives.cjs`) and a specialiser (`specialize-state-style.cjs`), and
|
|
10
|
-
// sharing those proves NOTHING about the answer each one gives — which is the whole gap. So what is
|
|
11
|
-
// shared here is the QUESTION and the EXPECTED ANSWER; the snippet that asks it stays per-framework,
|
|
12
|
-
// because `<View {...spread}>` and `<View v-bind="x">` are the same case in two syntaxes.
|
|
13
|
-
//
|
|
14
|
-
// TWO LEVELS, ONE VERDICT. Coverage is decided by INVOCATION — the style callback is called once
|
|
15
|
-
// per state and both results ride the bag as `style` + `activeStyle`, which is the same answer on
|
|
16
|
-
// all five adapters. Compile-time SUBSTITUTION (`specialize-state-style.cjs`) is an optimisation a
|
|
17
|
-
// transform applies when it can prove the body, saving a closure allocation; it never changes a
|
|
18
|
-
// verdict. So every row below is an equality across adapters even though what each one costs
|
|
19
|
-
// differs. A transform reporting `refuse` on a row the specialiser cannot prove has wired
|
|
20
|
-
// substitution as the MECHANISM rather than as the optimisation, and that is the drift this table
|
|
21
|
-
// exists to catch.
|
|
22
|
-
//
|
|
23
|
-
// A REFUSAL CATEGORY CAN BELONG TO THE TRANSFORM RATHER THAN TO THE LANGUAGE, and
|
|
24
|
-
// `REFUSAL_CATEGORIES.emitStyleExpressionOnce` is the worked example — and it carries that name
|
|
25
|
-
// because it was FIRST written as a refusal called `unrepeatableRead`, then corrected to a
|
|
26
|
-
// requirement on the output. It names `style={getStyle()}`,
|
|
27
|
-
// `style={bag[i]}`, `style={flag ? a : b}` — expressions that change meaning when read twice. But
|
|
28
|
-
// the pair is only built by reading TWICE if the transform prints the expression twice, which an
|
|
29
|
-
// inline guard (`typeof f === 'function' ? f({pressed}) : f`) does and a runtime helper
|
|
30
|
-
// (`resolveStateStyle(expr)`) does not: the helper reads the expression ONCE and calls its RESULT
|
|
31
|
-
// twice. Measured 2026-08-23 — Svelte lowers all three with exactly one read in the emitted text;
|
|
32
|
-
// Vue refused them while emitting the guard inline.
|
|
33
|
-
//
|
|
34
|
-
// So a row asserting `refuse` there would encode one transform's emit shape as a shared law and
|
|
35
|
-
// cost every other transform real coverage. Rows for these shapes belong in the table as `lower`,
|
|
36
|
-
// with "emit the expression once" stated as the requirement — which makes a double-reading
|
|
37
|
-
// transform FAIL the table instead of being ratified by it. The general form: before adding a
|
|
38
|
-
// refusal row, ask whether the hazard survives a different emit. If it does not, the row belongs
|
|
39
|
-
// to the emit.
|
|
40
|
-
//
|
|
41
|
-
// THE CONTRACT INVOCATION IMPOSES, stated because it is now observable: a style callback must be
|
|
42
|
-
// PURE in `pressed`. It is executed twice. A side-effecting body was already broken under
|
|
43
|
-
// substitution, but only invocation runs it.
|
|
44
|
-
//
|
|
45
|
-
// A `refuse` ROW IS UNPROVEN UNTIL A CONTROL ON THE SAME PRIMITIVE GOES THE OTHER WAY. `refuse` is
|
|
46
|
-
// the ABSENCE of an observation, and "no intrinsic in the output" is produced equally by a
|
|
47
|
-
// transform that refused and by a primitive nothing can lower — an unimported component, a
|
|
48
|
-
// hardcoded default element, an entry withheld from the spec while its runtime half is wired.
|
|
49
|
-
// Withholding is deliberate practice here, so the ambiguous state recurs by design. Measured
|
|
50
|
-
// 2026-08-31: the `TextInput` entry was withdrawn and both `intrinsic-choice-*` rows went GREEN on
|
|
51
|
-
// all three runners, in three different mechanisms. So a runner asserts a shape that MUST lower
|
|
52
|
-
// before it reads a refusal — and the control's failure message says the row cannot distinguish
|
|
53
|
-
// the two, which is the true state. Full account: `.claude/rules/adapter-parity-audit.md`.
|
|
54
|
-
//
|
|
55
|
-
// WHY IT MATTERS THAT NO OTHER AUDIT SEES THIS. Barrels, subpaths, `files` coverage and exported
|
|
56
|
-
// symbols are all untouched by a transform that lowers a call site its sibling refuses. Every suite
|
|
57
|
-
// stays green and the divergence surfaces either as one adapter being mysteriously slower, or as a
|
|
58
|
-
// button that lowered when it should not have and therefore does not respond.
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* @typedef {'lower' | 'refuse'} IVerdict
|
|
62
|
-
* @typedef {{ id: string, what: string, expected: IVerdict, why: string }} ILoweringCase
|
|
63
|
-
*/
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Every case a lowering transform must answer the same way. Each adapter's test supplies its own
|
|
67
|
-
* snippet per `id` and asserts `expected`; a case with no snippet is itself a failure, so adding a
|
|
68
|
-
* row here forces every transform to declare where it stands.
|
|
69
|
-
* @type {ReadonlyArray<ILoweringCase>}
|
|
70
|
-
*/
|
|
71
|
-
const LOWERING_CASES = [
|
|
72
|
-
{
|
|
73
|
-
id: 'inert-object-style',
|
|
74
|
-
what: 'style is an object literal',
|
|
75
|
-
expected: 'lower',
|
|
76
|
-
why: 'provably not a function, so the template cannot be reading press state through it',
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
id: 'hoisted-identifier-style',
|
|
80
|
-
what: 'style is a bare identifier that may hold an object OR a function',
|
|
81
|
-
expected: 'lower',
|
|
82
|
-
why: "the compile-time allow-list could never decide this one, which is why it used to refuse. INVOCATION decides it at runtime instead — `typeof f === 'function' ? f({pressed}) : f` — so the shape that no substitution can prove becomes the shape that needs no proof",
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
id: 'specialisable-state-style',
|
|
86
|
-
what: 'style is an arrow taking { pressed } and returning one object literal',
|
|
87
|
-
expected: 'lower',
|
|
88
|
-
why: 'INVOCATION covers it — the callback is called once per state and both results ride the bag as style + activeStyle. `specialize-state-style.cjs` is an OPTIMISATION on top for the bodies a transform can prove, saving the closure; it never changes the verdict',
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
id: 'nested-function-state-style',
|
|
92
|
-
what: 'the state style body contains another function',
|
|
93
|
-
expected: 'lower',
|
|
94
|
-
why: 'the SPECIALISER refuses this body — it cannot prove a nested function — but the verdict is set by invocation, which does not need to prove anything. The case stays in the table precisely because it separates the two levels: a transform that reports `refuse` here has wired substitution as the mechanism instead of as the optimisation',
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
id: 'call-expression-style',
|
|
98
|
-
what: 'style is a call expression',
|
|
99
|
-
expected: 'lower',
|
|
100
|
-
why: "`REFUSAL_CATEGORIES.emitStyleExpressionOnce` — safe exactly when the transform prints the expression ONCE and calls the RESULT twice. A transform printing an inline guard repeats it and runs the author's call once per copy per recompute; that is its emit to fix, not a shape to ban. Assert `occurrences(out, expr) === 1` on the output",
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
id: 'computed-member-style',
|
|
104
|
-
what: 'style is a computed member expression',
|
|
105
|
-
expected: 'lower',
|
|
106
|
-
why: 'same requirement as the call expression, and the index is the tell: printed twice, `bag[i]` is evaluated twice',
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
id: 'conditional-style',
|
|
110
|
-
what: 'style is a conditional expression',
|
|
111
|
-
expected: 'lower',
|
|
112
|
-
why: 'same requirement, and the sharpest failure of breaking it: printed twice, the two reads are free to take DIFFERENT branches, so the resting and pressed halves come from unrelated objects',
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
id: 'zero-arity-child',
|
|
116
|
-
what: 'children take no parameter',
|
|
117
|
-
expected: 'lower',
|
|
118
|
-
why: 'an ordinary lazy child, not a render prop. On Svelte EVERY child is a snippet whether the author wrote one or not, so arity is the only thing separating the two',
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
id: 'render-prop-child',
|
|
122
|
-
what: 'children take a parameter',
|
|
123
|
-
expected: 'refuse',
|
|
124
|
-
why: 'the parameter is the press state, and tier 2 resolves that state BELOW the framework where the template cannot read it back',
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
id: 'spread-attributes',
|
|
128
|
-
what: 'the element carries a spread',
|
|
129
|
-
expected: 'refuse',
|
|
130
|
-
why: 'the attribute set cannot be enumerated, and a half-read set is a silently wrong render',
|
|
131
|
-
},
|
|
132
|
-
// THE ROW THAT PROVES A CATEGORY IS A DICTIONARY, NOT AN ENFORCEMENT POINT.
|
|
133
|
-
//
|
|
134
|
-
// `REFUSAL_CATEGORIES.bagFold` said an element carrying `role` / `aria-*` must refuse, because
|
|
135
|
-
// the fold needs the whole bag and a transform reads one attribute at a time. Measured
|
|
136
|
-
// 2026-08-31, ONE of the four transforms implemented it — Solid. Vue's two lowered such elements
|
|
137
|
-
// and Svelte's preprocessor does not contain the string `role` at all.
|
|
138
|
-
//
|
|
139
|
-
// That was not a dead refusal, it was a live defect: a lowered `aria-label` reached Fabric as a
|
|
140
|
-
// key no ViewConfig knows, so the accessibility LABEL was silently dropped on device. A category
|
|
141
|
-
// written in the shared spec binds nobody — each transform separately decides to consult it, and
|
|
142
|
-
// not consulting it breaks nothing visible. Only a ROW here makes a divergence red.
|
|
143
|
-
//
|
|
144
|
-
// The verdict is `lower` because the fold now runs in the engine (`core/engine/src/
|
|
145
|
-
// accessibility-props.ts`, called from `fabricProps` — the one point where the whole bag is known
|
|
146
|
-
// on every commit path), so the reason to refuse is gone for every adapter at once. A transform
|
|
147
|
-
// still refusing is not being safe, it is losing coverage on the props real apps write.
|
|
148
|
-
// THE TAG CHOICE ITSELF IS DELIBERATELY NOT A ROW, and the reason is this table's own admission
|
|
149
|
-
// test (`.claude/rules/adapter-parity-audit.md`). A row's verdict vocabulary is `lower` /
|
|
150
|
-
// `refuse`; it cannot say WHICH intrinsic was emitted. So a row asserting that
|
|
151
|
-
// `<TextInput multiline />` lowers would pass against a transform emitting the single-line tag —
|
|
152
|
-
// and `symbiote-text-input` is a PREFIX of `symbiote-text-input-multiline`, so even a
|
|
153
|
-
// hand-written `toContain` check reads the wrong one as right. The tag choice is pinned by each
|
|
154
|
-
// adapter's own test, where the emitted text is available; all three carry one.
|
|
155
|
-
//
|
|
156
|
-
// What the table CAN decide is the refusal, and that is what these two rows are for: a dynamic
|
|
157
|
-
// selector must refuse on every transform, or one of them commits the wrong NATIVE VIEW — an
|
|
158
|
-
// error no later prop write can correct, unlike a merely wrong prop value.
|
|
159
|
-
{
|
|
160
|
-
id: 'intrinsic-choice-dynamic',
|
|
161
|
-
what: 'the intrinsic-selecting prop is a runtime value',
|
|
162
|
-
expected: 'refuse',
|
|
163
|
-
why: 'a transform prints a static tag, so a selector it cannot resolve at compile time leaves it guessing which native view to commit',
|
|
164
|
-
},
|
|
165
|
-
{
|
|
166
|
-
id: 'intrinsic-choice-nonboolean-literal',
|
|
167
|
-
what: 'the intrinsic-selecting prop is a truthy non-boolean literal',
|
|
168
|
-
expected: 'refuse',
|
|
169
|
-
why: 'the boundary is IDENTITY, not truthiness — a type-shaped check waves `multiline={1}` through and commits the multiline view for an author who wrote a number',
|
|
170
|
-
},
|
|
171
|
-
{
|
|
172
|
-
id: 'aria-bag-fold',
|
|
173
|
-
what: 'the element carries role / aria-* attributes',
|
|
174
|
-
expected: 'lower',
|
|
175
|
-
why: 'the fold moved to the engine, so a lowered element gets it too — a transform that still refuses only costs coverage, and three of the four never refused in the first place',
|
|
176
|
-
},
|
|
177
|
-
// THE VERDICT IS RIGHT AND THE REASON THIS ROW SHIPPED WITH WAS FALSE — kept as a comment because
|
|
178
|
-
// the correction is the more useful half. It read "a lowered element has no component instance
|
|
179
|
-
// for the binding to target", which assumes the binding targeted one before. It did not: NO
|
|
180
|
-
// adapter exposes a public `ref` on `Pressable`. React's `ref: viewRef`
|
|
181
|
-
// (`components/pressable/index.ts:204`) is internal, handed to the inner View so the machine can
|
|
182
|
-
// measure its retention region; Vue and Svelte declare none, and Solid's own props type says so
|
|
183
|
-
// out loud. So `ref={handle}` on an un-lowered `<Pressable>` does nothing at all.
|
|
184
|
-
//
|
|
185
|
-
// What lowering does is therefore not to BREAK the binding but to ADD one — the intrinsic hands
|
|
186
|
-
// back a live engine node. That is the actual hazard, and it is worse than the stated one: the
|
|
187
|
-
// capability would exist only when the transform happened to lower, so an unrelated attribute
|
|
188
|
-
// elsewhere on the tag would decide whether an app's `ref` works. A surface that flickers with a
|
|
189
|
-
// compiler's verdict is harder to reason about than one that is absent everywhere.
|
|
190
|
-
//
|
|
191
|
-
// Hence the rule this row now stands on, which generalises past `ref`: A LOWERING TRANSFORM IS AN
|
|
192
|
-
// OPTIMISATION, AND AN OPTIMISATION THAT CHANGES THE OBSERVABLE SURFACE — IN EITHER DIRECTION — IS
|
|
193
|
-
// A BUG. Refusing keeps lowered and un-lowered call sites indistinguishable to an app. If
|
|
194
|
-
// `Pressable` is later given a public ref, it is given one on all five adapters by design
|
|
195
|
-
// (`<adapters_reach_full_feature_parity>`), and only then can this row be revisited.
|
|
196
|
-
//
|
|
197
|
-
// Found by the Solid session 2026-08-30, when Solid's newly-added runner answered `lower` here
|
|
198
|
-
// and the investigation went looking for the instance the row assumed.
|
|
199
|
-
{
|
|
200
|
-
id: 'instance-bound-directive',
|
|
201
|
-
what: 'the element carries a directive binding the component instance',
|
|
202
|
-
expected: 'refuse',
|
|
203
|
-
why: 'lowering must not change the observable surface: no adapter exposes a public ref on Pressable, so lowering would ADD one that appears only when the transform happens to lower',
|
|
204
|
-
},
|
|
205
|
-
// The first FOLD-ONLY primitive, and the row exists because a transform genuinely decides it: an
|
|
206
|
-
// entry can be present in the spec and still be dropped between the file and the transform's own
|
|
207
|
-
// projection (`spec-projection-covers-fields.test.ts` exists because `intrinsicWhen` was), in
|
|
208
|
-
// which case the tag is simply never recognised and the element stays a component. Nothing else
|
|
209
|
-
// in this table would catch that.
|
|
210
|
-
//
|
|
211
|
-
// No attribute of its own on purpose: what is under test is that the NAME is recognised, not any
|
|
212
|
-
// rule about a prop. A row that needed an attribute would be testing two things at once.
|
|
213
|
-
{
|
|
214
|
-
id: 'image-fold-only',
|
|
215
|
-
what: 'a fold-only primitive with no attribute worth refusing',
|
|
216
|
-
expected: 'lower',
|
|
217
|
-
why: 'the spec carries the entry and the behavior carries the fold, so every transform should recognise the tag; a refusal here means the entry never reached this transform projection',
|
|
218
|
-
},
|
|
219
|
-
// A SECOND fold-only name, and it is not a duplicate of the row above. What that one proves is
|
|
220
|
-
// that a transform's spec projection works at all; what this proves is that it is driven by the
|
|
221
|
-
// spec rather than by a hardcoded list of names — the shape `adapters/angular` shipped for months
|
|
222
|
-
// (`LOWERABLE_NAMES = ['View', 'Text']`). A transform can pass `image-fold-only` and fail here.
|
|
223
|
-
{
|
|
224
|
-
id: 'input-accessory-view-fold-only',
|
|
225
|
-
what: 'a second fold-only primitive, proving the name list is read rather than written',
|
|
226
|
-
expected: 'lower',
|
|
227
|
-
why: 'nothing about this primitive is refusable — no state, no intrinsic choice, no aliasing — so a refusal can only mean the transform never saw the entry',
|
|
228
|
-
},
|
|
229
|
-
// A THIRD name, and it is not fold-only the way the two above are — it carries a real ENGINE
|
|
230
|
-
// machine (mirrors the last value native reported, sends a platform snap-back command on
|
|
231
|
-
// disagreement). What decides a TRANSFORM's verdict is `observesState`/`intrinsicWhen`, neither
|
|
232
|
-
// of which this entry sets (its public surface has no function-valued style and no dynamic
|
|
233
|
-
// intrinsic choice), so from a transform's perspective it is answered exactly like a fold-only
|
|
234
|
-
// primitive — the machine is an engine-side fact, invisible to this row. `thumbColor` is a
|
|
235
|
-
// CONSUMED alias (folds to `thumbTintColor`), same rationale as Image's `alt`.
|
|
236
|
-
{
|
|
237
|
-
id: 'switch-fold-only',
|
|
238
|
-
what: 'a third name with an engine machine but no compile-time refusal — the verdict does not depend on it',
|
|
239
|
-
expected: 'lower',
|
|
240
|
-
why: 'no observesState, no intrinsicWhen — a refusal here can only mean the transform never saw the entry, same as the two fold-only rows above',
|
|
241
|
-
},
|
|
242
|
-
];
|
|
243
|
-
|
|
244
|
-
// NO `safe-area-view` ROW, deliberately, and the reason belongs here or the next reader files it as
|
|
245
|
-
// a coverage gap. SafeAreaView is a third fold-only primitive, and the two rows above already spend
|
|
246
|
-
// that question: one proves a transform's spec projection works at all, the other proves the name
|
|
247
|
-
// list is READ rather than written. A third attribute-free name is answered identically by every
|
|
248
|
-
// implementation, including a broken one — the admission test in
|
|
249
|
-
// `.claude/rules/adapter-parity-audit.md` ("could a transform get this wrong?") says that is a row
|
|
250
|
-
// which proves nothing.
|
|
251
|
-
//
|
|
252
|
-
// It does carry one thing genuinely new — it is the first entry declaring `aliases: {}`, so a
|
|
253
|
-
// transform that folded `id -> nativeID` unconditionally would be wrong on it. That is NOT
|
|
254
|
-
// expressible here: this table's verdict is lower/refuse, and both the right and the wrong transform
|
|
255
|
-
// LOWER. It needs a payload oracle, which is per-adapter by construction; Svelte's is the pair in
|
|
256
|
-
// `preprocessor/lower-host-primitives.test.ts` ("folds no alias on a primitive whose spec declares
|
|
257
|
-
// none", with the control beside it).
|
|
258
|
-
|
|
259
|
-
module.exports = { LOWERING_CASES };
|
package/lowering-fixtures.d.cts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
// Hand-written types for the loose `.cjs` beside this file — Babel/Metro transforms cannot import
|
|
2
|
-
// `.ts`, so the table stays CommonJS and its shape is declared here.
|
|
3
|
-
|
|
4
|
-
export type IVerdict = 'lower' | 'refuse';
|
|
5
|
-
|
|
6
|
-
export interface ILoweringCase {
|
|
7
|
-
/** Stable key each adapter's test maps to its own snippet. */
|
|
8
|
-
id: string;
|
|
9
|
-
/** One line naming the construct, for a failure message that reads without the table open. */
|
|
10
|
-
what: string;
|
|
11
|
-
/** The answer every transform must give. */
|
|
12
|
-
expected: IVerdict;
|
|
13
|
-
/** Why that is the answer — the reasoning a transform author needs, not a restatement. */
|
|
14
|
-
why: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export declare const LOWERING_CASES: ReadonlyArray<ILoweringCase>;
|