@symbiote-native/solid 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 +9 -10
- package/babel-preset.cjs +0 -5
- package/build/components/activity-indicator-props.d.ts +5 -0
- package/build/components/activity-indicator-props.js +1 -0
- package/build/components/button-props.d.ts +5 -0
- package/build/components/button-props.js +1 -0
- package/build/components/flat-list/index.jsx +3 -3
- package/build/components/{image-background.d.ts → image-background-props.d.ts} +2 -3
- package/build/components/image-background-props.js +1 -0
- package/build/components/image-props.d.ts +5 -0
- package/build/components/image-props.js +6 -0
- package/build/components/{input-accessory-view.d.ts → input-accessory-view-props.d.ts} +1 -2
- package/build/components/input-accessory-view-props.js +5 -0
- package/build/components/keyboard-avoiding-view.jsx +9 -4
- package/build/components/modal.jsx +6 -6
- package/build/components/{pressable.d.ts → pressable-props.d.ts} +8 -7
- package/build/components/pressable-props.js +13 -0
- package/build/components/{refresh-control.d.ts → refresh-control-props.d.ts} +3 -3
- package/build/components/refresh-control-props.js +7 -0
- package/build/components/safe-area-view-props.d.ts +10 -0
- package/build/components/safe-area-view-props.js +5 -0
- package/build/components/{scroll-view/shared.d.ts → scroll-view-props.d.ts} +7 -13
- package/build/components/scroll-view-props.js +29 -0
- package/build/components/switch-props.d.ts +6 -0
- package/build/components/switch-props.js +5 -0
- package/build/components/text-input-props.d.ts +8 -0
- package/build/components/text-input-props.js +1 -0
- package/build/components/{text.d.ts → text-props.d.ts} +3 -3
- package/build/components/text-props.js +11 -0
- package/build/components/touchable-native-feedback/touchable-native-feedback-props.d.ts +8 -0
- package/build/components/touchable-native-feedback/touchable-native-feedback-props.js +1 -0
- package/build/components/touchable-props.d.ts +18 -0
- package/build/components/touchable-props.js +1 -0
- package/build/components/touchable-without-feedback/touchable-without-feedback-props.d.ts +6 -0
- package/build/components/touchable-without-feedback/touchable-without-feedback-props.js +1 -0
- package/build/components/{view.d.ts → view-props.d.ts} +2 -3
- package/build/components/view-props.js +11 -0
- package/build/components/virtualized-list/index.android.js +5 -4
- package/build/components/virtualized-list/index.ios.js +6 -6
- package/build/components/virtualized-list/shared.d.ts +1 -3
- package/build/components/virtualized-list/shared.jsx +87 -98
- package/build/components/virtualized-list/sticky-header.d.ts +6 -0
- package/build/components/{scroll-view → virtualized-list}/sticky-header.jsx +28 -36
- package/build/components.d.ts +20 -30
- package/build/components.js +6 -33
- package/build/create-portal/index.jsx +1 -1
- package/build/descriptor-to-solid.js +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.js +11 -3
- package/build/jsx-runtime.d.ts +39 -2
- package/build/modules/animated/index.d.ts +0 -4
- package/build/modules/animated/index.js +16 -22
- package/build/modules/image.d.ts +3 -0
- package/build/modules/image.js +12 -0
- package/build/register.d.ts +1 -1
- package/build/register.js +12 -28
- package/build/renderer.js +79 -16
- package/build/utils/stable-keys.d.ts +1 -1
- package/build/utils/stable-keys.js +6 -5
- package/metro-css-parser.cjs +2 -1
- package/package.json +5 -10
- package/babel-lower-host-primitives.cjs +0 -323
- package/build/components/activity-indicator/index.android.d.ts +0 -2
- package/build/components/activity-indicator/index.android.js +0 -11
- package/build/components/activity-indicator/index.d.ts +0 -1
- package/build/components/activity-indicator/index.ios.d.ts +0 -2
- package/build/components/activity-indicator/index.ios.js +0 -10
- package/build/components/activity-indicator/index.js +0 -4
- package/build/components/activity-indicator/shared.d.ts +0 -8
- package/build/components/activity-indicator/shared.js +0 -51
- package/build/components/button.d.ts +0 -7
- package/build/components/button.jsx +0 -45
- package/build/components/image-background.jsx +0 -74
- package/build/components/image.d.ts +0 -9
- package/build/components/image.js +0 -75
- package/build/components/input-accessory-view.jsx +0 -39
- package/build/components/pressable.jsx +0 -211
- package/build/components/refresh-control.jsx +0 -36
- package/build/components/safe-area-view.d.ts +0 -11
- package/build/components/safe-area-view.jsx +0 -24
- package/build/components/scroll-view/index.android.d.ts +0 -2
- package/build/components/scroll-view/index.android.js +0 -11
- package/build/components/scroll-view/index.d.ts +0 -2
- package/build/components/scroll-view/index.ios.d.ts +0 -2
- package/build/components/scroll-view/index.ios.js +0 -8
- package/build/components/scroll-view/index.js +0 -9
- package/build/components/scroll-view/shared.jsx +0 -334
- package/build/components/scroll-view/sticky-header.d.ts +0 -18
- package/build/components/switch/index.android.d.ts +0 -2
- package/build/components/switch/index.android.js +0 -12
- package/build/components/switch/index.d.ts +0 -1
- package/build/components/switch/index.ios.d.ts +0 -2
- package/build/components/switch/index.ios.js +0 -11
- package/build/components/switch/index.js +0 -4
- package/build/components/switch/shared.d.ts +0 -11
- package/build/components/switch/shared.js +0 -87
- package/build/components/text-input.d.ts +0 -10
- package/build/components/text-input.js +0 -290
- package/build/components/text.jsx +0 -27
- package/build/components/touchable/index.d.ts +0 -22
- package/build/components/touchable/index.jsx +0 -247
- package/build/components/touchable-native-feedback/index.d.ts +0 -16
- package/build/components/touchable-native-feedback/index.jsx +0 -68
- package/build/components/view.jsx +0 -49
- package/build/state-style.d.ts +0 -1
- package/build/state-style.js +0 -13
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
// Compile-time lowering of <View>/<Text> to the intrinsic tags the renderer maps to Fabric.
|
|
2
|
-
//
|
|
3
|
-
// WHY. Solid charges a props PROXY per component: `<View>` compiles to `createComponent(View, …)`,
|
|
4
|
-
// and the component body then runs splitProps + withStableKeys + mergeProps + spread. Measured
|
|
5
|
-
// 2026-08-23 on a 4 000-row create: keys/get/getOwnPropertyDescriptor traps 16.2% of the whole
|
|
6
|
-
// create, splitProps another 6.1%. Lowered, the same element compiles to `createElement(tag)` plus
|
|
7
|
-
// one `setProp` per attribute — no Proxy, no bag object, no spread render effect. Headless A/B with
|
|
8
|
-
// both arms compiled for real and createNode asserted equal at 36 002: -24.3% min / -30.0% median.
|
|
9
|
-
//
|
|
10
|
-
// The Vue twin is the nodeTransform in adapters/vue/metro-vue-transformer.cjs, and the two share
|
|
11
|
-
// their central rule: lower ONLY a tag this file imported from us, never a bare name match, or an
|
|
12
|
-
// app's own <View> gets rewritten.
|
|
13
|
-
//
|
|
14
|
-
// THE SECOND RULE IS THE ONE THAT KEEPS IT HONEST: lower only when the WHOLE attribute set is
|
|
15
|
-
// visible. The wrapper components do real work besides forwarding — resolveAccessibilityProps
|
|
16
|
-
// folds aria-*/role into the composite accessibilityState/accessibilityValue, and `id` is RN's
|
|
17
|
-
// W3C alias for `nativeID`. Deleting the wrapper without carrying those over breaks accessibility
|
|
18
|
-
// silently. `id` is a pure rename and is done below FROM THE SHARED SPEC; the aria fold is a BAG
|
|
19
|
-
// operation, so an
|
|
20
|
-
// element carrying aria-*/role — or a spread whose keys cannot be read — is left as a component.
|
|
21
|
-
// A folded-at-compile-time aria bag is a possible follow-up (the bag exists statically, so it
|
|
22
|
-
// would cost nothing at runtime); refusing is what makes shipping this safe without it.
|
|
23
|
-
//
|
|
24
|
-
// .cjs because Babel require()s it and this package is "type": "module".
|
|
25
|
-
|
|
26
|
-
// The tag map and the per-tag folds come from the SHARED spec, not from a copy here. Four
|
|
27
|
-
// transforms carried their own before it existed and they had already disagreed on `id` (see that
|
|
28
|
-
// file's `aliases` comment). Data only — every line of AST work below stays this adapter's, and so
|
|
29
|
-
// does the whole refusal mechanism; only the SPECIFICATION is shared.
|
|
30
|
-
const {
|
|
31
|
-
HOST_PRIMITIVES,
|
|
32
|
-
REFUSAL_CATEGORIES,
|
|
33
|
-
} = require('@symbiote-native/components/host-primitives');
|
|
34
|
-
|
|
35
|
-
const SOURCE = '@symbiote-native/solid';
|
|
36
|
-
|
|
37
|
-
// name -> a projection of the shared spec. Built once at plugin load, so the per-element path stays
|
|
38
|
-
// two Map lookups.
|
|
39
|
-
//
|
|
40
|
-
// This is a WHITELIST, and it fails silently in one direction: a field the shared spec grows and
|
|
41
|
-
// this list does not copy simply never reaches the detections, which then behave as if it were
|
|
42
|
-
// absent. Cost measured 2026-08-31 — `intrinsicWhen` was implemented end to end and every
|
|
43
|
-
// multiline case still lowered to the single-line tag, because the field was dropped here. Add a
|
|
44
|
-
// field to the spec, add it here.
|
|
45
|
-
const LOWERABLE = new Map(
|
|
46
|
-
Object.entries(HOST_PRIMITIVES).map(([name, spec]) => [
|
|
47
|
-
name,
|
|
48
|
-
{
|
|
49
|
-
intrinsic: spec.intrinsic,
|
|
50
|
-
aliases: new Map(Object.entries(spec.aliases)),
|
|
51
|
-
// Every primitive refuses on the bag fold and on anything unreadable. A primitive whose
|
|
52
|
-
// STATE the template can observe refuses on more, and only the spec knows which those are —
|
|
53
|
-
// `observesState` is set for a tag the shared spec marks that way, so a transform that has
|
|
54
|
-
// not implemented the extra detections simply never sees such a tag in its map.
|
|
55
|
-
observesState: spec.observesState === true,
|
|
56
|
-
// The two-intrinsic selector (`{ prop, intrinsic }`), absent for a primitive with one tag.
|
|
57
|
-
intrinsicWhen: spec.intrinsicWhen,
|
|
58
|
-
},
|
|
59
|
-
]),
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
function attributeName(attribute) {
|
|
63
|
-
const { name } = attribute;
|
|
64
|
-
if (name.type === 'JSXIdentifier') return name.name;
|
|
65
|
-
// JSXNamespacedName — `aria-label` parses as an identifier, but `xml:lang` style names land here.
|
|
66
|
-
return `${name.namespace.name}:${name.name.name}`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Every local name bound to a lowerable export of @symbiote-native/solid in THIS file, honouring
|
|
70
|
-
// `import { View as Box }`. A file that imports nothing from us yields an empty map and is skipped
|
|
71
|
-
// whole.
|
|
72
|
-
function lowerableLocalNames(programPath) {
|
|
73
|
-
const names = new Map();
|
|
74
|
-
programPath.node.body.forEach(statement => {
|
|
75
|
-
if (
|
|
76
|
-
statement.type !== 'ImportDeclaration' ||
|
|
77
|
-
statement.source.value !== SOURCE
|
|
78
|
-
)
|
|
79
|
-
return;
|
|
80
|
-
statement.specifiers.forEach(specifier => {
|
|
81
|
-
if (specifier.type !== 'ImportSpecifier') return;
|
|
82
|
-
const imported =
|
|
83
|
-
specifier.imported.type === 'Identifier'
|
|
84
|
-
? specifier.imported.name
|
|
85
|
-
: specifier.imported.value;
|
|
86
|
-
const spec = LOWERABLE.get(imported);
|
|
87
|
-
if (spec !== undefined) names.set(specifier.local.name, spec);
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
return names;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// REFUSAL_CATEGORIES.renderPropChild — a child function that TAKES the state.
|
|
94
|
-
//
|
|
95
|
-
// ARITY, not `typeof`, and that distinction is the whole rule. A zero-argument arrow is an ordinary
|
|
96
|
-
// Solid child (`JSX.Element` covers `() => Element`), exactly as `<For>`'s map fn is; only a
|
|
97
|
-
// function that accepts the state argument is a render prop. Measured over `examples/solid`: 12 of
|
|
98
|
-
// the 19 `<Pressable>` sites pass a function child and TEN of those are zero-arity, so refusing on
|
|
99
|
-
// `typeof` alone would throw away ten lowerable sites — including `ActionButton`, whose one
|
|
100
|
-
// definition is instantiated 146 times.
|
|
101
|
-
function isRenderPropChild(child) {
|
|
102
|
-
if (child.type !== 'JSXExpressionContainer') return false;
|
|
103
|
-
const { expression } = child;
|
|
104
|
-
return (
|
|
105
|
-
(expression.type === 'ArrowFunctionExpression' ||
|
|
106
|
-
expression.type === 'FunctionExpression') &&
|
|
107
|
-
expression.params.length >= 1
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// A JSX element is lowerable only if every attribute is a plain, named one we can account for —
|
|
112
|
-
// plus, for a state-observing primitive, only if nothing in it reads that state.
|
|
113
|
-
function canLower(element, spec, types) {
|
|
114
|
-
const { openingElement } = element;
|
|
115
|
-
// Named attributes only — a spread cannot be enumerated
|
|
116
|
-
// (REFUSAL_CATEGORIES.unreadableAttributeSet).
|
|
117
|
-
//
|
|
118
|
-
// The category's STATED reason — that a transform cannot fold `id` -> `nativeID` inside a bag — is
|
|
119
|
-
// DEAD here, measured 2026-08-31: this adapter also folds at runtime (`renderer.ts`,
|
|
120
|
-
// `foldAliasKey`), which sees every key whatever shape it arrived in, and a lowered
|
|
121
|
-
// `<symbiote-view {...bag} />` commits a payload byte-identical to the wrapper's
|
|
122
|
-
// (`src/spread-fold-parity.test.tsx`). The refusal stands on two OTHER grounds it never named,
|
|
123
|
-
// both measured by lifting it and compiling:
|
|
124
|
-
//
|
|
125
|
-
// <TextInput {...bag} /> -> symbiote-text-input, whatever `bag.multiline` holds. The
|
|
126
|
-
// single-line tag is a different NATIVE VIEW and no later prop
|
|
127
|
-
// write repairs it (REFUSAL_CATEGORIES.dynamicIntrinsicChoice).
|
|
128
|
-
// <Pressable {...bag} /> -> no `activeStyle` emitted. The same functional style written
|
|
129
|
-
// directly emits it; inside a bag the expansion never runs, so a
|
|
130
|
-
// pressed style silently stops existing.
|
|
131
|
-
//
|
|
132
|
-
// So: a refuted rationale is not a refuted verdict. Do not lift this because the alias half is
|
|
133
|
-
// covered — check what ELSE reads the attribute list first.
|
|
134
|
-
//
|
|
135
|
-
// `role` / `aria-*` used to refuse here too, on the
|
|
136
|
-
// grounds that their fold needs to see sibling keys and a per-key element path cannot reproduce
|
|
137
|
-
// it. That was true of the ELEMENT path and is no longer true of anything: the fold moved into
|
|
138
|
-
// the engine (`core/engine/src/accessibility-props.ts`, called from `fabricProps` — the one place
|
|
139
|
-
// the whole bag is visible on both commit paths), so a lowered element gets it exactly as a
|
|
140
|
-
// wrapped one does. Proven on the committed payload, both arms, in `src/aria-fold-parity.test.tsx`.
|
|
141
|
-
const attributesAreReadable = openingElement.attributes.every(
|
|
142
|
-
attribute => attribute.type === 'JSXAttribute',
|
|
143
|
-
);
|
|
144
|
-
if (!attributesAreReadable) return false;
|
|
145
|
-
if (!selectsIntrinsicStatically(openingElement, spec)) return false;
|
|
146
|
-
// BEFORE the `observesState` early-return, not after. It rode below it while the only ref-refusing
|
|
147
|
-
// primitive was Pressable, which observes state — a coincidence, and TextInput broke it: it
|
|
148
|
-
// observes nothing, so the early-return fired first and a `<TextInput ref>` lowered anyway. The
|
|
149
|
-
// two questions are independent, so the checks are ordered by what they ask, not by what happened
|
|
150
|
-
// to work.
|
|
151
|
-
if (refWouldChangeSurface(spec) && openingElement.attributes.some(isInstanceBinding))
|
|
152
|
-
return false;
|
|
153
|
-
// Everything below is about a primitive whose STATE the template can observe.
|
|
154
|
-
if (!spec.observesState) return true;
|
|
155
|
-
// A functional `style` NEVER refuses, and since 2026-09-01 it is not rewritten either: the
|
|
156
|
-
// expression rides through unchanged and `routeProp` resolves it at both values of `pressed`
|
|
157
|
-
// (`isStyleCallback`, `core/engine/src/node.ts`). The build-time split that used to live here was
|
|
158
|
-
// removed after measuring it — `src/state-style-cost.bench.test.ts` — because it emitted TWO
|
|
159
|
-
// writes per node where the callback emits one, so it cost run time rather than saving it.
|
|
160
|
-
return !element.children.some(isRenderPropChild);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// REFUSAL_CATEGORIES.dynamicIntrinsicChoice.
|
|
164
|
-
//
|
|
165
|
-
// A primitive whose `intrinsicWhen` is set picks between TWO Fabric views by a prop —
|
|
166
|
-
// `symbiote-text-input` vs `symbiote-text-input-multiline`, which are different native views rather
|
|
167
|
-
// than one view with a flag (`core/components/src/view/render-text-input.ts`). The transform prints
|
|
168
|
-
// a STATIC tag name, so it can only make that choice when the value is a compile-time literal.
|
|
169
|
-
//
|
|
170
|
-
// The refusal is stricter than the neighbouring ones and the difference is worth keeping in view:
|
|
171
|
-
// an unreadable ATTRIBUTE VALUE means a prop arrives wrong, and a later write can still correct it.
|
|
172
|
-
// Here the wrong NATIVE VIEW is committed, and no subsequent prop write repairs that — the node
|
|
173
|
-
// would have to be destroyed and recreated. So a value this cannot read statically keeps the
|
|
174
|
-
// component, which resolves the choice at runtime where the value is known.
|
|
175
|
-
//
|
|
176
|
-
// IDENTITY, not truthiness. The render fn is `view.multiline ? MULTILINE : SINGLE`, so truthiness
|
|
177
|
-
// is what the RUNTIME does, and describing the compile-time check that way makes `multiline={1}`
|
|
178
|
-
// look like something this should resolve. It must not: only three shapes are accepted — a bare
|
|
179
|
-
// attribute is `true`, a boolean LITERAL is itself, an absent prop is `false` — and everything else
|
|
180
|
-
// refuses, a truthy non-boolean literal included. A check justified by truthiness would let `{1}`
|
|
181
|
-
// through at the next edit, which is exactly the silently-wrong native view the refusal exists to
|
|
182
|
-
// prevent. Recorded as identity in the shared spec for the same reason.
|
|
183
|
-
function staticTruthOf(openingElement, propName) {
|
|
184
|
-
const attribute = openingElement.attributes.find(
|
|
185
|
-
candidate =>
|
|
186
|
-
candidate.type === 'JSXAttribute' &&
|
|
187
|
-
attributeName(candidate) === propName,
|
|
188
|
-
);
|
|
189
|
-
if (attribute === undefined) return false;
|
|
190
|
-
if (attribute.value === null) return true;
|
|
191
|
-
if (attribute.value.type !== 'JSXExpressionContainer') return undefined;
|
|
192
|
-
const { expression } = attribute.value;
|
|
193
|
-
return expression.type === 'BooleanLiteral' ? expression.value : undefined;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function selectsIntrinsicStatically(openingElement, spec) {
|
|
197
|
-
return (
|
|
198
|
-
spec.intrinsicWhen === undefined ||
|
|
199
|
-
staticTruthOf(openingElement, spec.intrinsicWhen.prop) !== undefined
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function intrinsicFor(openingElement, spec) {
|
|
204
|
-
if (spec.intrinsicWhen === undefined) return spec.intrinsic;
|
|
205
|
-
return staticTruthOf(openingElement, spec.intrinsicWhen.prop) === true
|
|
206
|
-
? spec.intrinsicWhen.intrinsic
|
|
207
|
-
: spec.intrinsic;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// REFUSAL_CATEGORIES.instanceBoundDirective. The criterion is NOT "does the component expose a
|
|
211
|
-
// ref" — that was a proxy, and it held only while two primitives existed. The rule is: would
|
|
212
|
-
// lowering hand the app something DIFFERENT from what the component hands it? A lowering transform
|
|
213
|
-
// is an optimisation, and an optimisation that moves the observable surface in EITHER direction is
|
|
214
|
-
// a bug. Three shapes, one rule, all readable from the components' own props types:
|
|
215
|
-
//
|
|
216
|
-
// View, Text ref?: Ref<IHostInstance> lowered yields the same node -> lower
|
|
217
|
-
// Pressable no ref declared lowering would ADD a handle -> refuse
|
|
218
|
-
// TextInput ref?: Ref<ITextInputHandle> lowering would SWAP the handle -> refuse
|
|
219
|
-
//
|
|
220
|
-
// TextInput is the case that exposed the proxy: it declares a ref, so the old criterion said
|
|
221
|
-
// "lower", and a lowered `<TextInput ref>` silently dropped `clear`, `isFocused` and
|
|
222
|
-
// `setSelection` — the component's handle replaced by a bare public instance. Costing an app three
|
|
223
|
-
// methods is exactly as wrong as granting it one it never had.
|
|
224
|
-
//
|
|
225
|
-
// PER-ADAPTER by construction, which is why this is a list here and not a field on the shared
|
|
226
|
-
// spec: what a ref yields is a fact about THIS adapter's props types, and a correct adapter can
|
|
227
|
-
// answer differently — Vue refuses on every primitive, because a Vue template ref yields the
|
|
228
|
-
// component instance rather than the host node whatever the component declares.
|
|
229
|
-
//
|
|
230
|
-
// Kept honest by `ref-refusal-matches-components.test.ts`, which re-derives the answer from each
|
|
231
|
-
// component source: a hand-written list of adapter members is what went stale three times this
|
|
232
|
-
// month.
|
|
233
|
-
// INVERTED 2026-09-01, and the inversion is the point rather than the contents. This was a
|
|
234
|
-
// denylist of intrinsics that refuse a `ref`, so every primitive added to the spec defaulted to
|
|
235
|
-
// LOWERING one — and the newest member falls out of a hand-written list every time, which is the
|
|
236
|
-
// failure mode this repo has now recorded four times. As a denylist that default is a correctness
|
|
237
|
-
// bug (a lowered element hands back an object the component never did); as an allowlist the same
|
|
238
|
-
// omission costs only coverage, which is the direction a default should fail in.
|
|
239
|
-
//
|
|
240
|
-
// A name earns its place here by the criterion in `ref-refusal-matches-components.test.ts`: the
|
|
241
|
-
// component hands a `ref` the ENGINE NODE, either by declaring `ref?: Ref<IHostInstance>` or by
|
|
242
|
-
// rendering through `descriptorToSolid`, whose `spread` calls a bag ref with the node it built. Any
|
|
243
|
-
// other answer — a different handle, or no ref at all — means lowering would move the surface.
|
|
244
|
-
const INTRINSICS_YIELDING_HOST_REF = new Set([
|
|
245
|
-
'symbiote-view',
|
|
246
|
-
'symbiote-text',
|
|
247
|
-
'symbiote-image',
|
|
248
|
-
]);
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
function refWouldChangeSurface(spec) {
|
|
252
|
-
return !INTRINSICS_YIELDING_HOST_REF.has(spec.intrinsic);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function isInstanceBinding(attribute) {
|
|
256
|
-
return (
|
|
257
|
-
attribute.type === 'JSXAttribute' && attributeName(attribute) === 'ref'
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Per TAG, not global: the spec gives each primitive its own alias map, and reading it per tag is
|
|
262
|
-
// what keeps this honest if they ever stop agreeing.
|
|
263
|
-
function renameAliasedAttributes(openingElement, aliases, types) {
|
|
264
|
-
openingElement.attributes.forEach(attribute => {
|
|
265
|
-
// A spread has no name, and the author's own spread refuses in `canLower`, so nothing this
|
|
266
|
-
// loop sees is one. It used to be reachable: the state-style pass emitted a spread for an
|
|
267
|
-
// opaque style, and that pass is gone.
|
|
268
|
-
if (attribute.type !== 'JSXAttribute') return;
|
|
269
|
-
const renamed = aliases.get(attributeName(attribute));
|
|
270
|
-
if (renamed !== undefined) attribute.name = types.jsxIdentifier(renamed);
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
module.exports = function lowerHostPrimitives({ types }) {
|
|
275
|
-
return {
|
|
276
|
-
name: 'symbiote-solid-lower-host-primitives',
|
|
277
|
-
visitor: {
|
|
278
|
-
Program(programPath) {
|
|
279
|
-
const names = lowerableLocalNames(programPath);
|
|
280
|
-
if (names.size === 0) return;
|
|
281
|
-
programPath.traverse({
|
|
282
|
-
JSXElement(elementPath) {
|
|
283
|
-
const { openingElement, closingElement } = elementPath.node;
|
|
284
|
-
const tag = openingElement.name;
|
|
285
|
-
if (tag.type !== 'JSXIdentifier') return;
|
|
286
|
-
const spec = names.get(tag.name);
|
|
287
|
-
if (spec === undefined) return;
|
|
288
|
-
// A shadowing local binding (a parameter or a `const View = …` in scope) is NOT our
|
|
289
|
-
// import, even though the name matches. Only rewrite when the binding at this use site
|
|
290
|
-
// is still the module-level import.
|
|
291
|
-
const binding = elementPath.scope.getBinding(tag.name);
|
|
292
|
-
if (binding === undefined || binding.kind !== 'module') return;
|
|
293
|
-
if (!canLower(elementPath.node, spec, types)) return;
|
|
294
|
-
// Read BEFORE the passes below rewrite attributes — `renameAliasedAttributes` could
|
|
295
|
-
// in principle rename the selector out from under the choice.
|
|
296
|
-
const intrinsic = intrinsicFor(openingElement, spec);
|
|
297
|
-
renameAliasedAttributes(openingElement, spec.aliases, types);
|
|
298
|
-
openingElement.name = types.jsxIdentifier(intrinsic);
|
|
299
|
-
if (closingElement)
|
|
300
|
-
closingElement.name = types.jsxIdentifier(intrinsic);
|
|
301
|
-
},
|
|
302
|
-
});
|
|
303
|
-
},
|
|
304
|
-
},
|
|
305
|
-
};
|
|
306
|
-
};
|
|
307
|
-
|
|
308
|
-
// Attached AFTER the plugin assignment on purpose: `module.exports = fn` replaces the object, so a
|
|
309
|
-
// named export written above it is silently discarded. Exists only so the test below can compare
|
|
310
|
-
// this list against the component sources.
|
|
311
|
-
module.exports.INTRINSICS_YIELDING_HOST_REF = INTRINSICS_YIELDING_HOST_REF;
|
|
312
|
-
|
|
313
|
-
// The spec fields this transform reads, named so `spec-projection-covers-fields.test.ts` can check
|
|
314
|
-
// the whitelist above against what `HOST_PRIMITIVES` actually carries. `defaults` is deliberately
|
|
315
|
-
// absent: prop defaults are seeded at RUNTIME by the renderer (`seedTextDefaults`), where a patch
|
|
316
|
-
// that clears a prop can restore them — a compile-time copy would cover only the initial render.
|
|
317
|
-
module.exports.SPEC_FIELDS_READ = [
|
|
318
|
-
'intrinsic',
|
|
319
|
-
'aliases',
|
|
320
|
-
'observesState',
|
|
321
|
-
'intrinsicWhen',
|
|
322
|
-
];
|
|
323
|
-
module.exports.SPEC_FIELDS_IGNORED = ['defaults'];
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
// ActivityIndicator on Android: AndroidProgressBar needs `styleAttr` (it drives its setStyle();
|
|
2
|
-
// without it the view throws "setStyle() not called") plus `indeterminate: true`, and its default
|
|
3
|
-
// color is the theme (null), which the render fn turns into an OMITTED color prop — a literal null
|
|
4
|
-
// would be rejected by Fabric's color parser. Same facts the other adapters' Android bindings
|
|
5
|
-
// encode. Metro picks this on an Android host; no Platform.OS read.
|
|
6
|
-
import { createActivityIndicator } from './shared.js';
|
|
7
|
-
export const ActivityIndicator = createActivityIndicator({
|
|
8
|
-
// RN: `color = Platform.OS === 'ios' ? GRAY : null`; Android lets the theme color it.
|
|
9
|
-
defaultColor: null,
|
|
10
|
-
nativeExtras: { styleAttr: 'Normal', indeterminate: true },
|
|
11
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './index.ios';
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
// ActivityIndicator on iOS: RCTActivityIndicatorView takes the size enum plus a GRAY default color
|
|
2
|
-
// and no extra native props — the same facts adapters/react's, adapters/vue's and adapters/svelte's
|
|
3
|
-
// iOS bindings encode. Metro selects this file on an iOS host, no Platform.OS read.
|
|
4
|
-
import { createActivityIndicator } from './shared.js';
|
|
5
|
-
// RN's iOS default spinner color (Libraries/.../ActivityIndicator.js GRAY).
|
|
6
|
-
const IOS_DEFAULT_COLOR = '#999999';
|
|
7
|
-
export const ActivityIndicator = createActivityIndicator({
|
|
8
|
-
defaultColor: IOS_DEFAULT_COLOR,
|
|
9
|
-
nativeExtras: {},
|
|
10
|
-
});
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { JSX } from '../../jsx-runtime';
|
|
2
|
-
import { type IActivityIndicatorPlatform, type IActivityIndicatorProps as IActivityIndicatorBaseProps } from '@symbiote-native/components';
|
|
3
|
-
import type { IClassNameValue } from '@symbiote-native/engine';
|
|
4
|
-
export type { IActivityIndicatorPlatform, IActivityIndicatorSize, } from '@symbiote-native/components';
|
|
5
|
-
export type IActivityIndicatorProps = IActivityIndicatorBaseProps & {
|
|
6
|
-
class?: IClassNameValue;
|
|
7
|
-
};
|
|
8
|
-
export declare function createActivityIndicator(platform: IActivityIndicatorPlatform): (props: IActivityIndicatorProps) => JSX.Element;
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
// ActivityIndicator, the Solid lifecycle half. The render (the 'small'/'large' -> native size enum
|
|
2
|
-
// translation, the fixed size boxes, the centering wrapper, the color omission) lives in
|
|
3
|
-
// @symbiote-native/components/view and is shared verbatim with React, Vue and Svelte. There is no
|
|
4
|
-
// state machine: the spinner animates natively, so Solid supplies only prop resolution and the
|
|
5
|
-
// descriptor bridge.
|
|
6
|
-
//
|
|
7
|
-
// NOTHING here destructures `props`. Solid props are getters and a component body runs ONCE;
|
|
8
|
-
// destructuring reads them at setup and freezes the component at its mount-time values. Every read
|
|
9
|
-
// below happens inside the descriptor accessor, which descriptorToSolid re-runs as a render effect
|
|
10
|
-
// on the SAME host nodes.
|
|
11
|
-
//
|
|
12
|
-
// The per-platform bits (iOS's GRAY default and no extras; Android's theme/null default plus
|
|
13
|
-
// styleAttr + indeterminate) arrive via `platform` from index.ios.ts / index.android.ts.
|
|
14
|
-
import { renderActivityIndicator, resolveAccessibilityProps, } from '@symbiote-native/components';
|
|
15
|
-
import { descriptorToSolid } from '../../descriptor-to-solid.js';
|
|
16
|
-
// Read by the component itself; everything else (testID, nativeID, the accessibility surface,
|
|
17
|
-
// onLayout, class) forwards onto the centering wrapper View, which is where RN spreads `...props`.
|
|
18
|
-
const HANDLED_PROPS = [
|
|
19
|
-
'animating',
|
|
20
|
-
'color',
|
|
21
|
-
'hidesWhenStopped',
|
|
22
|
-
'size',
|
|
23
|
-
'style',
|
|
24
|
-
];
|
|
25
|
-
// The wrapper is a raw symbiote-view emitted by the render fn, not the View component, so it never
|
|
26
|
-
// runs resolveAccessibilityProps itself — the fold happens here, exactly like React's hook does.
|
|
27
|
-
// The two branches of that fold emit DIFFERENT key sets; descriptorToSolid runs the resulting bag
|
|
28
|
-
// through withStableKeys, so a vanished key still reaches routeProp as a delete
|
|
29
|
-
// (.claude/rules/solid-descriptor-bridge.md §1).
|
|
30
|
-
function forwardProps(props) {
|
|
31
|
-
const forwarded = {
|
|
32
|
-
...resolveAccessibilityProps(props),
|
|
33
|
-
};
|
|
34
|
-
for (const key of HANDLED_PROPS)
|
|
35
|
-
delete forwarded[key];
|
|
36
|
-
return forwarded;
|
|
37
|
-
}
|
|
38
|
-
export function createActivityIndicator(platform) {
|
|
39
|
-
return function ActivityIndicator(props) {
|
|
40
|
-
// `?? 'small'` / `?? true` rather than a `!== false` coercion: props are typed here, so this is
|
|
41
|
-
// the exact semantics of React's destructuring defaults (only `undefined` falls back).
|
|
42
|
-
return descriptorToSolid(() => renderActivityIndicator({
|
|
43
|
-
animating: props.animating ?? true,
|
|
44
|
-
hidesWhenStopped: props.hidesWhenStopped ?? true,
|
|
45
|
-
size: props.size ?? 'small',
|
|
46
|
-
color: props.color,
|
|
47
|
-
style: props.style,
|
|
48
|
-
passthrough: forwardProps(props),
|
|
49
|
-
}, platform));
|
|
50
|
-
};
|
|
51
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { JSX } from '../jsx-runtime';
|
|
2
|
-
import { type IButtonProps as IButtonBaseProps } from '@symbiote-native/components';
|
|
3
|
-
import type { IClassNameValue } from '@symbiote-native/engine';
|
|
4
|
-
export type IButtonProps = IButtonBaseProps & {
|
|
5
|
-
class?: IClassNameValue;
|
|
6
|
-
};
|
|
7
|
-
export declare function Button(props: IButtonProps): JSX.Element;
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
// Button — the minimal cross-platform button, rendered in its iOS shape per RN's Button.js: a
|
|
2
|
-
// TouchableOpacity wrapping a Text. The base text style, the role constant and the color fold
|
|
3
|
-
// (a caller `color` tints the label; `disabled` greys it, disabled winning) are shared in
|
|
4
|
-
// @symbiote-native/components' render-button and are called, never re-derived; Solid only composes
|
|
5
|
-
// its own TouchableOpacity + Text and forwards the native-only props.
|
|
6
|
-
//
|
|
7
|
-
// NO descriptorToSolid here, and none in React/Vue/Svelte either: the tree is two COMPONENTS, not
|
|
8
|
-
// a Descriptor. There is no renderButton() to bridge — the shared half is the style/role math.
|
|
9
|
-
//
|
|
10
|
-
// NOTHING here destructures `props` at setup. Solid props are getters and a component body runs
|
|
11
|
-
// ONCE; splitProps keeps both halves reactive and every read sits inside an accessor or a memo.
|
|
12
|
-
import { createMemo, splitProps } from 'solid-js';
|
|
13
|
-
import { BUTTON_ACCESSIBILITY_ROLE, resolveButtonTextStyle, } from '@symbiote-native/components';
|
|
14
|
-
import { Text } from './text.jsx';
|
|
15
|
-
import { TouchableOpacity } from './touchable/index.jsx';
|
|
16
|
-
export function Button(props) {
|
|
17
|
-
// Only what Button CONSUMES or re-maps is split off. Everything else — onPress, disabled,
|
|
18
|
-
// testID, the TV-focus props, the accessibility/aria surface, `class` — stays in `rest` and
|
|
19
|
-
// rides the single bag below onto the TouchableOpacity. The TV-focus props are real Fabric
|
|
20
|
-
// props the pressable forwards without typing; a JSX spread carries them through unchecked
|
|
21
|
-
// (they are inert on a phone host).
|
|
22
|
-
const [local, rest] = splitProps(props, [
|
|
23
|
-
'title',
|
|
24
|
-
'color',
|
|
25
|
-
'touchSoundDisabled',
|
|
26
|
-
]);
|
|
27
|
-
const textStyle = createMemo(() => resolveButtonTextStyle(local.color, rest.disabled));
|
|
28
|
-
// ONE bag, not a spread followed by explicit props on the tag: Solid's mergeProps (which the
|
|
29
|
-
// compiler uses for that shape) takes the first NON-undefined value scanning back to front, so
|
|
30
|
-
// an override that can be undefined silently loses (.claude/rules/solid-descriptor-bridge.md
|
|
31
|
-
// §6). A plain object is last-wins — the React semantics this is ported from, and what pins RN's
|
|
32
|
-
// fixed role / accessible / disabled-state OVER the caller's own. touchSoundDisabled is RE-MAPPED
|
|
33
|
-
// here, not forwarded: the pressable spells it android_disableSound. Return type left INFERRED;
|
|
34
|
-
// widening to Record<string, unknown> would stop typing `onPress` too.
|
|
35
|
-
const touchableProps = () => ({
|
|
36
|
-
...rest,
|
|
37
|
-
android_disableSound: local.touchSoundDisabled,
|
|
38
|
-
accessibilityRole: BUTTON_ACCESSIBILITY_ROLE,
|
|
39
|
-
accessible: true,
|
|
40
|
-
accessibilityState: { disabled: rest.disabled },
|
|
41
|
-
});
|
|
42
|
-
return (<TouchableOpacity {...touchableProps()}>
|
|
43
|
-
<Text style={textStyle()}>{local.title}</Text>
|
|
44
|
-
</TouchableOpacity>);
|
|
45
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
// ImageBackground — the Solid lifecycle half. The composition (the absolute-fill Image behind the
|
|
2
|
-
// children, the wrapper-dimension proxy onto that Image, the imageStyle-last merge) lives
|
|
3
|
-
// framework-agnostic in @symbiote-native/components' renderImageBackground and is shared verbatim
|
|
4
|
-
// with React, Vue and Svelte. Solid supplies only the reactivity, the class-name resolution and
|
|
5
|
-
// the child order.
|
|
6
|
-
//
|
|
7
|
-
// SPLIT DOWN THE MIDDLE, and the seam is the children rule. The wrapper hosts a live Solid subtree
|
|
8
|
-
// (the overlay content), which only the reconciler can reduce, so it stays a literal
|
|
9
|
-
// `symbiote-view` carrying the shared fn's own props — the same shape View and SafeAreaView use,
|
|
10
|
-
// and the same one Svelte's ImageBackground lands on. The inner image is a leaf built entirely
|
|
11
|
-
// from VALUES, so it goes through descriptorToSolid, which builds it once and re-props it in place
|
|
12
|
-
// (replacing it would restart the download and drop the decoded bitmap).
|
|
13
|
-
//
|
|
14
|
-
// FLAT FILE, no folder: there are no platform or shared variants to group
|
|
15
|
-
// (`symbiote-file-layout` — only genuine groups get a folder).
|
|
16
|
-
//
|
|
17
|
-
// NOTHING here destructures `props` at setup. Solid props are getters and a component body runs
|
|
18
|
-
// ONCE; splitProps keeps every bucket reactive, and each read happens inside an accessor.
|
|
19
|
-
import { createMemo, splitProps } from 'solid-js';
|
|
20
|
-
import { createDescriptorShapeGuard, renderImageBackground, resolveAccessibilityProps, } from '@symbiote-native/components';
|
|
21
|
-
import { resolveClassName, } from '@symbiote-native/engine';
|
|
22
|
-
import { descriptorToSolid } from '../descriptor-to-solid.js';
|
|
23
|
-
// renderImage's typed view fields — the props it CONSUMES rather than forwards. Split off so they
|
|
24
|
-
// never also ride through `passthrough`: the W3C aliases in particular must not reach Fabric raw,
|
|
25
|
-
// since native sees only the resolved `source` array they fold into. `style` is absent on purpose
|
|
26
|
-
// — the inner image's style is computed by renderImageBackground, never taken from the caller.
|
|
27
|
-
const IMAGE_VIEW_PROPS = [
|
|
28
|
-
'source',
|
|
29
|
-
'defaultSource',
|
|
30
|
-
'loadingIndicatorSource',
|
|
31
|
-
'resizeMode',
|
|
32
|
-
'tintColor',
|
|
33
|
-
'src',
|
|
34
|
-
'srcSet',
|
|
35
|
-
'alt',
|
|
36
|
-
'width',
|
|
37
|
-
'height',
|
|
38
|
-
'crossOrigin',
|
|
39
|
-
'referrerPolicy',
|
|
40
|
-
];
|
|
41
|
-
const shape = createDescriptorShapeGuard('ImageBackground');
|
|
42
|
-
export function ImageBackground(props) {
|
|
43
|
-
const [local, view, rest] = splitProps(props, ['children', 'style', 'imageStyle', 'class'], IMAGE_VIEW_PROPS);
|
|
44
|
-
// One memo, so the wrapper bag and the inner image derive from a SINGLE call per change.
|
|
45
|
-
const wrapper = createMemo(() => renderImageBackground({
|
|
46
|
-
style: local.style,
|
|
47
|
-
// A bare string is a registered class name and has to become a style object here, before
|
|
48
|
-
// it reaches the shared fn — which merges it, and knows nothing about the registry.
|
|
49
|
-
imageStyle: typeof local.imageStyle === 'string'
|
|
50
|
-
? resolveClassName(local.imageStyle)
|
|
51
|
-
: local.imageStyle,
|
|
52
|
-
image: {
|
|
53
|
-
...view,
|
|
54
|
-
// The wrapper is a raw host, not the View component, so the aria/role fold is this
|
|
55
|
-
// component's own job; the resolved surface rides the IMAGE, matching every other adapter.
|
|
56
|
-
passthrough: { ...resolveAccessibilityProps(rest) },
|
|
57
|
-
},
|
|
58
|
-
}));
|
|
59
|
-
const backgroundImage = descriptorToSolid(() => shape.asElement(wrapper().children[0]));
|
|
60
|
-
// ONE object literal rather than a spread-then-override on the tag: `class` is legitimately
|
|
61
|
-
// undefined, and Solid's mergeProps (which the compiler uses for a spread followed by an
|
|
62
|
-
// explicit prop) would discard that undefined instead of applying it
|
|
63
|
-
// (.claude/rules/solid-descriptor-bridge.md §6). No withStableKeys — renderImageBackground's
|
|
64
|
-
// wrapper props are `{ style }` unconditionally, so no key can vanish between runs.
|
|
65
|
-
const wrapperBag = () => ({
|
|
66
|
-
...wrapper().props,
|
|
67
|
-
class: local.class,
|
|
68
|
-
});
|
|
69
|
-
// The user children come AFTER the image in child order, which is what makes them paint on top.
|
|
70
|
-
return (<symbiote-view {...wrapperBag()}>
|
|
71
|
-
{backgroundImage}
|
|
72
|
-
{local.children}
|
|
73
|
-
</symbiote-view>);
|
|
74
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { JSX } from '../jsx-runtime';
|
|
2
|
-
import { type IImageProps as IImageBaseProps, type IImageStatics } from '@symbiote-native/components';
|
|
3
|
-
import type { IClassNameValue } from '@symbiote-native/engine';
|
|
4
|
-
export type { IImageStatics } from '@symbiote-native/components';
|
|
5
|
-
export type IImageProps = IImageBaseProps & {
|
|
6
|
-
class?: IClassNameValue;
|
|
7
|
-
};
|
|
8
|
-
export type IImageWithStatics = ((props: IImageProps) => JSX.Element) & IImageStatics;
|
|
9
|
-
export declare const Image: IImageWithStatics;
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
// Image — the Solid lifecycle half. Everything that turns props into a native prop bag (the
|
|
2
|
-
// source / src / srcSet resolution, the width+height fold into style, resizeMode and tintColor
|
|
3
|
-
// read back off the style, alt -> accessibilityLabel, the array shape native expects) lives
|
|
4
|
-
// framework-agnostic in @symbiote-native/components' renderImage, shared verbatim with React, Vue
|
|
5
|
-
// and Svelte. Solid supplies only the reactivity and the Descriptor bridge.
|
|
6
|
-
//
|
|
7
|
-
// PLAIN .ts, NOT .tsx, unlike ./view.tsx and ./text.tsx — and the difference is the children rule,
|
|
8
|
-
// not a style choice. View and Text take a live subtree of the user's components, which only the
|
|
9
|
-
// framework's own reconciler can reduce, so they emit real JSX. An image is a leaf built entirely
|
|
10
|
-
// from VALUES, so it goes through renderImage + descriptorToSolid exactly like Switch does
|
|
11
|
-
// (.claude/rules/component-render-fn-boundary.md).
|
|
12
|
-
//
|
|
13
|
-
// NOTHING here destructures `props`. Solid props are getters and a component body runs ONCE;
|
|
14
|
-
// splitProps is the idiomatic split that keeps both halves reactive, and every read below happens
|
|
15
|
-
// inside the descriptor accessor descriptorToSolid re-runs.
|
|
16
|
-
//
|
|
17
|
-
// ASSET RESOLUTION IS LAZY BY CONSTRUCTION, and that is what lets an app drop the hand-rolled
|
|
18
|
-
// resolveImageSource() call it needed while this component did not exist. renderImage calls
|
|
19
|
-
// resolveImageSource at RENDER time, and the resolver behind it is installed by bootstrapHost,
|
|
20
|
-
// which createApp().mount() runs before AppRegistry ever renders a root. So a `require('./x.png')`
|
|
21
|
-
// asset id handed to `source` is resolved by RN's own resolveAssetSource on the way to Fabric,
|
|
22
|
-
// with no ordering hazard: a module-scope resolve would run one step too early, an accessor-time
|
|
23
|
-
// one cannot.
|
|
24
|
-
import { splitProps } from 'solid-js';
|
|
25
|
-
import { imageStatics, renderImage, resolveAccessibilityProps, } from '@symbiote-native/components';
|
|
26
|
-
import { descriptorToSolid } from '../descriptor-to-solid.js';
|
|
27
|
-
// renderImage's typed view fields — the props it CONSUMES rather than forwards. Split off here so
|
|
28
|
-
// they never also ride through `passthrough`: the W3C aliases in particular must not reach Fabric
|
|
29
|
-
// raw, since native sees only the resolved `source` array they fold into. Everything else (the
|
|
30
|
-
// load events, blurRadius, capInsets, resizeMethod, fadeDuration, testID, `class`, a caller's own
|
|
31
|
-
// `ref`, the accessibility props) stays in `rest` and lands on the host image untouched.
|
|
32
|
-
const VIEW_PROPS = [
|
|
33
|
-
'source',
|
|
34
|
-
'defaultSource',
|
|
35
|
-
'loadingIndicatorSource',
|
|
36
|
-
'style',
|
|
37
|
-
'resizeMode',
|
|
38
|
-
'tintColor',
|
|
39
|
-
'src',
|
|
40
|
-
'srcSet',
|
|
41
|
-
'alt',
|
|
42
|
-
'width',
|
|
43
|
-
'height',
|
|
44
|
-
'crossOrigin',
|
|
45
|
-
'referrerPolicy',
|
|
46
|
-
];
|
|
47
|
-
function ImageComponent(props) {
|
|
48
|
-
const [view, rest] = splitProps(props, VIEW_PROPS);
|
|
49
|
-
// The node is created once and kept by identity — descriptorToSolid wires every prop through a
|
|
50
|
-
// render effect on that same node, so a later source/style/alt change re-commits the existing
|
|
51
|
-
// image rather than replacing it (which would restart the download and drop the decoded bitmap).
|
|
52
|
-
return descriptorToSolid(() => renderImage({
|
|
53
|
-
source: view.source,
|
|
54
|
-
defaultSource: view.defaultSource,
|
|
55
|
-
loadingIndicatorSource: view.loadingIndicatorSource,
|
|
56
|
-
style: view.style,
|
|
57
|
-
resizeMode: view.resizeMode,
|
|
58
|
-
tintColor: view.tintColor,
|
|
59
|
-
src: view.src,
|
|
60
|
-
srcSet: view.srcSet,
|
|
61
|
-
alt: view.alt,
|
|
62
|
-
width: view.width,
|
|
63
|
-
height: view.height,
|
|
64
|
-
crossOrigin: view.crossOrigin,
|
|
65
|
-
referrerPolicy: view.referrerPolicy,
|
|
66
|
-
// Image owns its host element rather than rendering through a symbiote View, so folding
|
|
67
|
-
// aria/role into the canonical accessibility* props is its own job, like Switch's.
|
|
68
|
-
// renderImage's own `alt` fold then defers to an explicit accessibilityLabel found here.
|
|
69
|
-
passthrough: { ...resolveAccessibilityProps(rest) },
|
|
70
|
-
}));
|
|
71
|
-
}
|
|
72
|
-
// Statics attached like RN's own `Image.getSize` / `prefetch` / `queryCache` / `abortPrefetch` /
|
|
73
|
-
// `getSizeWithHeaders` / `resolveAssetSource`, by identity off the same @symbiote-native/engine
|
|
74
|
-
// module React, Vue and Svelte attach — never a Solid-side reimplementation.
|
|
75
|
-
export const Image = Object.assign(ImageComponent, imageStatics);
|