@symbiote-native/vue 0.4.0 → 1.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 +24 -9
- package/babel-jsx.cjs +53 -0
- package/babel-lower-host-primitives.cjs +225 -0
- package/build/bootstrap.js +1 -1
- package/build/components/button.js +4 -2
- package/build/components/flat-list/index.js +14 -5
- package/build/components/image-background.js +4 -5
- package/build/components/image.js +3 -1
- package/build/components/keyboard-avoiding-view.js +39 -14
- package/build/components/modal/index.js +5 -2
- package/build/components/pressable.d.ts +1 -1
- package/build/components/pressable.js +39 -20
- package/build/components/safe-area-view.d.ts +1 -0
- package/build/components/scroll-view/index.android.d.ts +1 -1
- package/build/components/scroll-view/index.android.js +3 -1
- package/build/components/scroll-view/index.ios.d.ts +1 -1
- package/build/components/scroll-view/index.ios.js +3 -1
- package/build/components/scroll-view/shared.js +33 -13
- package/build/components/scroll-view/sticky-header.js +18 -5
- package/build/components/section-list/index.js +4 -2
- package/build/components/switch/shared.js +12 -3
- package/build/components/text-input/index.js +25 -2
- package/build/components/touchable-native-feedback.js +11 -4
- package/build/components/touchable.d.ts +7 -1
- package/build/components/touchable.js +184 -33
- package/build/components/virtualized-list/index.js +42 -20
- package/build/components/virtualized-section-list/index.d.ts +5 -0
- package/build/components/virtualized-section-list/index.js +33 -4
- package/build/components.d.ts +2 -0
- package/build/components.js +25 -4
- package/build/composables/use-raw-attrs.d.ts +1 -0
- package/build/composables/use-raw-attrs.js +26 -0
- package/build/create-portal/index.d.ts +28 -0
- package/build/create-portal/index.js +42 -0
- package/build/host-instance/index.js +7 -6
- package/build/index.d.ts +9 -6
- package/build/index.js +14 -3
- package/build/modules/animated/create-animated-component.d.ts +1 -1
- package/build/modules/animated/create-animated-component.js +27 -5
- package/build/modules/animated/index.d.ts +2 -0
- package/build/modules/animated/index.js +18 -5
- package/build/modules/app-registry/index.js +4 -2
- package/build/modules/status-bar.js +4 -2
- package/build/register.d.ts +1 -0
- package/build/register.js +35 -0
- package/build/render.d.ts +3 -1
- package/build/render.js +62 -1
- package/build/renderer/index.js +58 -9
- package/build/runtime-helpers/index.d.ts +48 -24
- package/build/runtime-helpers/index.js +314 -30
- package/build/state-style.d.ts +1 -0
- package/build/state-style.js +13 -0
- package/build/utils/normalize-attrs.d.ts +1 -0
- package/build/utils/normalize-attrs.js +21 -13
- package/metro-css-parser.cjs +2 -1
- package/metro-vue-transformer.cjs +502 -101
- package/package.json +29 -8
|
@@ -1,28 +1,52 @@
|
|
|
1
1
|
export * from '@vue/runtime-core';
|
|
2
2
|
import { type ObjectDirective } from '@vue/runtime-core';
|
|
3
3
|
import { type ISymbioteNode } from '@symbiote-native/engine';
|
|
4
|
+
export { Teleport, type ITeleportTarget } from '../create-portal';
|
|
5
|
+
export declare function normalizeProps(props: Record<string, unknown> | null | undefined): Record<string, unknown> | null;
|
|
6
|
+
export declare function normalizeStyle(value: unknown): unknown;
|
|
7
|
+
export declare function mergeProps(...args: Record<string, unknown>[]): Record<string, unknown>;
|
|
4
8
|
export declare const vShow: ObjectDirective<ISymbioteNode, boolean>;
|
|
5
|
-
export declare const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
9
|
+
export declare const vModelText: ObjectDirective<ISymbioteNode, unknown>;
|
|
10
|
+
type ISystemModifier = 'ctrl' | 'shift' | 'alt' | 'meta';
|
|
11
|
+
type IEventModifier = ISystemModifier | 'stop' | 'prevent' | 'self' | 'left' | 'middle' | 'right' | 'exact';
|
|
12
|
+
type IModifierGuardableEvent = {
|
|
13
|
+
stopPropagation?: () => void;
|
|
14
|
+
preventDefault?: () => void;
|
|
15
|
+
target?: unknown;
|
|
16
|
+
currentTarget?: unknown;
|
|
17
|
+
button?: number;
|
|
18
|
+
ctrlKey?: boolean;
|
|
19
|
+
shiftKey?: boolean;
|
|
20
|
+
altKey?: boolean;
|
|
21
|
+
metaKey?: boolean;
|
|
22
|
+
};
|
|
23
|
+
type IEventHandler<TEvent> = ((event: TEvent, ...args: never[]) => unknown) & {
|
|
24
|
+
_withMods?: Record<string, IEventHandler<TEvent>>;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Render-function/compiled-template equivalent of `v-on.stop`/`.prevent`/`.self`/etc. `fn` is
|
|
28
|
+
* typed as required, matching upstream Vue's own declaration — its runtime-only `!fn` guard below
|
|
29
|
+
* (for a compiler-generated call site that could pass a falsy handler) isn't reflected in the
|
|
30
|
+
* type there either.
|
|
31
|
+
*/
|
|
32
|
+
export declare function withModifiers<TEvent extends IModifierGuardableEvent>(fn: IEventHandler<TEvent>, modifiers: readonly IEventModifier[]): IEventHandler<TEvent>;
|
|
33
|
+
type IKeyedEvent = {
|
|
34
|
+
key?: string;
|
|
35
|
+
};
|
|
36
|
+
type IKeyedHandler<TEvent extends IKeyedEvent> = ((event: TEvent) => unknown) & {
|
|
37
|
+
_withKeys?: Record<string, (event: TEvent) => unknown>;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Render-function/compiled-template equivalent of `v-on.enter`/`.esc`/etc. No-ops on any event
|
|
41
|
+
* without a `.key` field (e.g. Symbiote's Pressable `onPress`) — same as upstream Vue's own
|
|
42
|
+
* behavior on a non-keyboard DOM event, not a Symbiote-specific gap. `TextInput`'s `onKeyPress`
|
|
43
|
+
* carries `nativeEvent.key`, so that is the applicable Symbiote event shape.
|
|
44
|
+
*/
|
|
45
|
+
export declare function withKeys<TEvent extends IKeyedEvent>(fn: IKeyedHandler<TEvent>, modifiers: readonly string[]): (event: TEvent) => unknown;
|
|
46
|
+
/**
|
|
47
|
+
* Reads the class map a `<style module>` SFC block compiles onto the component's own options
|
|
48
|
+
* (`__cssModules`, set by @symbiote-native/css-parser's SFC compiler — see the
|
|
49
|
+
* `symbiote-sfc-style-compiler` skill). Warns and returns an empty map exactly like upstream Vue
|
|
50
|
+
* does when the current component has no such block, rather than throwing.
|
|
51
|
+
*/
|
|
52
|
+
export declare function useCssModule(name?: string): Record<string, string>;
|
|
@@ -6,11 +6,100 @@
|
|
|
6
6
|
// real HTMLElement - we have no DOM, so this module supplies our OWN implementation under the
|
|
7
7
|
// same export name instead of leaving `v-show` a silent no-op.
|
|
8
8
|
//
|
|
9
|
-
// Scope:
|
|
10
|
-
// v-model
|
|
9
|
+
// Scope: Vue's own template directives (v-show, Teleport) and, since host-primitive lowering
|
|
10
|
+
// landed, `v-model` on an ELEMENT — see vModelText below for why that stopped being out of scope.
|
|
11
11
|
export * from '@vue/runtime-core';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
12
|
+
import { getCurrentInstance, mergeProps as baseMergeProps, normalizeClass, normalizeStyle as baseNormalizeStyle, warn, } from '@vue/runtime-core';
|
|
13
|
+
import { requestCommitFor, setNativeProps, setProp, whenCommitted, } from '@symbiote-native/engine';
|
|
14
|
+
import { descriptorFor, SWITCH_TAG } from '@symbiote-native/components';
|
|
15
|
+
// Shadows runtime-core's own Teleport for every compiled `from 'vue'` import (see the header):
|
|
16
|
+
// same component, plus a runtime guard on `to`. Lives in ../create-portal so the adapter's portal
|
|
17
|
+
// sits where every other adapter's does; re-exported here because THIS is the module Metro
|
|
18
|
+
// rewrites `vue` to, and an `export *` alone would resolve `Teleport` to the unguarded original.
|
|
19
|
+
export { Teleport } from '../create-portal/index.js';
|
|
20
|
+
// Shadows @vue/shared's own `normalizeProps` for every compiled `from 'vue'` import, for ONE
|
|
21
|
+
// divergence: a FUNCTION-valued `style` survives it.
|
|
22
|
+
//
|
|
23
|
+
// `v-bind="obj"` compiles to `_normalizeProps(_guardReactiveProps(obj))`, and Vue's version runs
|
|
24
|
+
// `normalizeStyle` over the style key. That helper knows array / string / object and returns
|
|
25
|
+
// `undefined` for a function — so `style: ({ pressed }) => …` arriving inside a spread is destroyed
|
|
26
|
+
// before any adapter code runs, with no error and nothing red. A functional style is a React Native
|
|
27
|
+
// idiom the engine resolves in `routeProp` (`isStyleCallback`); Vue's normaliser is DOM-shaped and
|
|
28
|
+
// cannot know that.
|
|
29
|
+
//
|
|
30
|
+
// Measured 2026-09-01 through the real SFC pipeline, which is the ONLY path that reaches this:
|
|
31
|
+
// `h(Component, { ...bag })` skips `normalizeProps` entirely and shows the style arriving intact,
|
|
32
|
+
// so a probe written that way reports the bug as absent.
|
|
33
|
+
//
|
|
34
|
+
// <View v-bind="bag"> fn style committed {testID} <- style gone
|
|
35
|
+
// <Pressable v-bind="bag"> fn style committed {testID} <- style gone
|
|
36
|
+
// either, object style committed {testID, opacity} <- fine
|
|
37
|
+
//
|
|
38
|
+
// EVERY primitive, not only the stateful ones — both arms measured. Only `style`: `normalizeClass`
|
|
39
|
+
// has no equivalent hazard that anyone has shown, and an override wider than the measured defect is
|
|
40
|
+
// how one adapter's problem becomes a shared law.
|
|
41
|
+
//
|
|
42
|
+
// MIRRORS Vue's four lines rather than delegating to them, because hiding the key from the original
|
|
43
|
+
// and restoring it afterwards would mutate the caller's object twice. The normalisation itself is
|
|
44
|
+
// still Vue's — only the dispatch is ours, and `normalize-props.test.ts` pins it against the
|
|
45
|
+
// compiled path.
|
|
46
|
+
export function normalizeProps(props) {
|
|
47
|
+
if (!props)
|
|
48
|
+
return null;
|
|
49
|
+
const { class: klass, style } = props;
|
|
50
|
+
if (klass && typeof klass !== 'string')
|
|
51
|
+
props.class = normalizeClass(klass);
|
|
52
|
+
if (style && typeof style !== 'function')
|
|
53
|
+
props.style = baseNormalizeStyle(style);
|
|
54
|
+
return props;
|
|
55
|
+
}
|
|
56
|
+
// The SECOND door to the same defect, and the one the first fix missed. `v-bind="rest"` standing
|
|
57
|
+
// beside a separate `:style="fn"` does not compile to `normalizeProps` at all — it compiles to
|
|
58
|
+
// `mergeProps`, whose own `style` branch is `ret.style = normalizeStyle([ret.style, toMerge.style])`.
|
|
59
|
+
// The array branch of `normalizeStyle` drops a function element, so the callback dies here too:
|
|
60
|
+
//
|
|
61
|
+
// <View v-bind="bag" /> normalizeProps <- fixed above
|
|
62
|
+
// <View v-bind="rest" :style="fn" /> mergeProps <- fixed here
|
|
63
|
+
// <View :style="fn" /> neither <- was never broken
|
|
64
|
+
//
|
|
65
|
+
// Delegates the whole merge — classes, event-handler concatenation, key precedence — and only
|
|
66
|
+
// rescues the callback afterwards, scanning the args in mergeProps' own order so LAST wins. A
|
|
67
|
+
// later non-function `style` clears it again, which is that same precedence.
|
|
68
|
+
//
|
|
69
|
+
// The merge itself cannot be preserved when one side is a callback: a function returns a COMPLETE
|
|
70
|
+
// style, so there is nothing to merge it into. It replaces, which is also how React Native's own
|
|
71
|
+
// contract reads it (`Pressable.js`: `ViewStyleProp | ((state) => ViewStyleProp)` — the callback is
|
|
72
|
+
// top-level, never an array element, so `:style="[a, fn]"` is out of contract here exactly as it is
|
|
73
|
+
// there, and is deliberately NOT rescued).
|
|
74
|
+
// The THIRD door, and the one that decides whether the state-style split can leave the transforms
|
|
75
|
+
// at all. A `:style` binding on a lowered element compiles to `_normalizeStyle(expr)` whenever the
|
|
76
|
+
// compiler cannot keep it on the cheap patch-flag path — an inline arrow and a call expression both
|
|
77
|
+
// do, a bare identifier does not. So `<Pressable :style="({pressed}) => …" />` loses its callback
|
|
78
|
+
// here, and today nothing notices because the lowering transform rewrites that attribute into a
|
|
79
|
+
// resting/active pair before Vue ever emits the helper.
|
|
80
|
+
//
|
|
81
|
+
// A TOP-LEVEL function is preserved; one nested inside an array is not, and that asymmetry is the
|
|
82
|
+
// contract rather than an omission. React Native types the prop
|
|
83
|
+
// `ViewStyleProp | ((state) => ViewStyleProp)` (`Pressable.js`), so the callback is top-level or it
|
|
84
|
+
// is not a callback, and the engine's `isStyleCallback` reads it the same way. Rescuing an array
|
|
85
|
+
// element would invent a shape neither side supports.
|
|
86
|
+
export function normalizeStyle(value) {
|
|
87
|
+
if (typeof value === 'function')
|
|
88
|
+
return value;
|
|
89
|
+
return baseNormalizeStyle(value);
|
|
90
|
+
}
|
|
91
|
+
export function mergeProps(...args) {
|
|
92
|
+
const merged = baseMergeProps(...args);
|
|
93
|
+
let callback;
|
|
94
|
+
for (const arg of args) {
|
|
95
|
+
if (arg === null || arg === undefined || !('style' in arg))
|
|
96
|
+
continue;
|
|
97
|
+
callback = typeof arg.style === 'function' ? arg.style : undefined;
|
|
98
|
+
}
|
|
99
|
+
if (callback !== undefined)
|
|
100
|
+
merged.style = callback;
|
|
101
|
+
return merged;
|
|
102
|
+
}
|
|
14
103
|
// whenCommitted, not a direct call: Vue's `mounted` hook fires synchronously during the patch
|
|
15
104
|
// pass, but this renderer coalesces the actual Fabric commit onto a microtask
|
|
16
105
|
// (surface.requestCommit()), so the node may have no committed tag yet - the same async-commit
|
|
@@ -28,31 +117,226 @@ export const vShow = {
|
|
|
28
117
|
// Drop a still-pending first-commit wait if the element unmounts before it ever lands.
|
|
29
118
|
unmounted: el => pendingShowCommits.get(el)?.(),
|
|
30
119
|
};
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
120
|
+
const modelStates = new WeakMap();
|
|
121
|
+
// Runtime guards, not casts: both slots are read out of untyped bags — `vnode.props` and
|
|
122
|
+
// `node.props` are `Record<string, unknown>` — and `typeof x === 'function'` alone narrows to the
|
|
123
|
+
// bare `Function`, which carries no signature.
|
|
124
|
+
function isModelAssign(value) {
|
|
125
|
+
return typeof value === 'function';
|
|
126
|
+
}
|
|
127
|
+
function isValueChangeListener(value) {
|
|
128
|
+
return typeof value === 'function';
|
|
129
|
+
}
|
|
130
|
+
// Vue merges duplicate handlers into an array, so the assigner is either a function or a list of
|
|
131
|
+
// them — the same shape upstream's `getModelAssigner` normalises.
|
|
132
|
+
function modelAssigner(props) {
|
|
133
|
+
const bound = props?.['onUpdate:modelValue'] ?? props?.['onUpdate:value'];
|
|
134
|
+
if (isModelAssign(bound))
|
|
135
|
+
return bound;
|
|
136
|
+
if (!Array.isArray(bound))
|
|
137
|
+
return undefined;
|
|
138
|
+
const fns = bound.filter(isModelAssign);
|
|
139
|
+
return value => fns.forEach(fn => fn(value));
|
|
140
|
+
}
|
|
141
|
+
function modelText(value) {
|
|
142
|
+
if (typeof value === 'string')
|
|
143
|
+
return value;
|
|
144
|
+
return value === undefined || value === null ? '' : String(value);
|
|
145
|
+
}
|
|
146
|
+
// `.trim` and `.number` are pure transforms of the text and behave exactly as upstream. `.lazy` is
|
|
147
|
+
// NOT: upstream implements it by listening to `change` instead of `input`, and this renderer has
|
|
148
|
+
// only the one native stream (RN fires `onChange` per keystroke), so there is no second event to
|
|
149
|
+
// switch to. It warns rather than silently behaving like the eager form.
|
|
150
|
+
function applyModelModifiers(state, text) {
|
|
151
|
+
const trimmed = state.trim ? text.trim() : text;
|
|
152
|
+
if (!state.number)
|
|
153
|
+
return trimmed;
|
|
154
|
+
const parsed = Number.parseFloat(trimmed);
|
|
155
|
+
return Number.isNaN(parsed) ? trimmed : parsed;
|
|
156
|
+
}
|
|
157
|
+
function modelStateFor(el) {
|
|
158
|
+
const existing = modelStates.get(el);
|
|
159
|
+
if (existing !== undefined)
|
|
160
|
+
return existing;
|
|
161
|
+
const state = {
|
|
162
|
+
assign: undefined,
|
|
163
|
+
appListener: undefined,
|
|
164
|
+
trim: false,
|
|
165
|
+
number: false,
|
|
166
|
+
listener: (text, event) => {
|
|
167
|
+
state.appListener?.(text, event);
|
|
168
|
+
state.assign?.(applyModelModifiers(state, text));
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
modelStates.set(el, state);
|
|
172
|
+
return state;
|
|
173
|
+
}
|
|
174
|
+
// Re-read every beat rather than installed once: the app's own `onValueChange` binding is written
|
|
175
|
+
// by `patchProp` during the same patch, so anything captured earlier is either missing or stale. A
|
|
176
|
+
// prop that is already OUR listener means nothing overwrote it and the app half is unchanged.
|
|
177
|
+
function syncModelListener(el, state) {
|
|
178
|
+
const current = el.props.onValueChange;
|
|
179
|
+
if (current === state.listener)
|
|
180
|
+
return;
|
|
181
|
+
state.appListener = isValueChangeListener(current) ? current : undefined;
|
|
182
|
+
setProp(el, 'onValueChange', state.listener);
|
|
183
|
+
}
|
|
184
|
+
// Vue's compiler picks the directive by ELEMENT, and for anything it does not recognise as a DOM
|
|
185
|
+
// input it emits `vModelText` — including a lowered `<symbiote-switch>`. Stringifying there is what
|
|
186
|
+
// upstream must do (a DOM input's value IS a string) and what we must not: the Switch behavior
|
|
187
|
+
// reads `props.value === true`, so `String(true)` pins the control OFF and no tap can move it.
|
|
188
|
+
// Device-confirmed on `examples/vue-sfc` 2026-09-02, both switches on `CanaryScreen`.
|
|
189
|
+
//
|
|
190
|
+
// Resolved through the shared descriptor table rather than a literal Fabric name, so the check
|
|
191
|
+
// follows the platform the bundle actually loaded (iOS `Switch`, Android `AndroidSwitch`) instead
|
|
192
|
+
// of carrying a copy of both.
|
|
193
|
+
const SWITCH_COMPONENT = descriptorFor(SWITCH_TAG).component;
|
|
194
|
+
function isSwitchNode(el) {
|
|
195
|
+
return el.component === SWITCH_COMPONENT;
|
|
196
|
+
}
|
|
197
|
+
function syncModelValue(el, value) {
|
|
198
|
+
// The READ half needs no branch: the behavior calls `onValueChange(value, event)` for both
|
|
199
|
+
// primitives, and `applyModelModifiers` passes a non-string through untouched.
|
|
200
|
+
if (isSwitchNode(el)) {
|
|
201
|
+
setProp(el, 'value', value === true);
|
|
202
|
+
requestCommitFor(el);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
setProp(el, 'value', modelText(value));
|
|
206
|
+
// The behavior's `afterCommit` is what turns a changed `value` into the stale-safe
|
|
207
|
+
// setTextAndSelection command, and nothing else in this patch is going to ask for a commit — the
|
|
208
|
+
// template no longer carries a `value` binding at all once `v-model` owns it.
|
|
209
|
+
requestCommitFor(el);
|
|
210
|
+
}
|
|
211
|
+
export const vModelText = {
|
|
212
|
+
// `created` runs BEFORE props are patched, which is exactly what the value half needs: the
|
|
213
|
+
// behavior seeds its `lastNativeText` mirror from `value` on the FIRST commit, and a value that
|
|
214
|
+
// arrives after that seed reads as a divergence and commands redundant text down to native.
|
|
215
|
+
created(el, { value, modifiers }) {
|
|
216
|
+
const state = modelStateFor(el);
|
|
217
|
+
state.trim = modifiers.trim === true;
|
|
218
|
+
state.number = modifiers.number === true;
|
|
219
|
+
if (modifiers.lazy === true) {
|
|
220
|
+
warn('v-model.lazy has no effect here: this renderer has one native text stream, so there is no ' +
|
|
221
|
+
'second event to defer to.');
|
|
222
|
+
}
|
|
223
|
+
syncModelValue(el, value);
|
|
224
|
+
},
|
|
225
|
+
mounted(el, { value }, vnode) {
|
|
226
|
+
const state = modelStateFor(el);
|
|
227
|
+
state.assign = modelAssigner(vnode.props);
|
|
228
|
+
syncModelListener(el, state);
|
|
229
|
+
syncModelValue(el, value);
|
|
230
|
+
},
|
|
231
|
+
updated(el, { value }, vnode) {
|
|
232
|
+
const state = modelStateFor(el);
|
|
233
|
+
state.assign = modelAssigner(vnode.props);
|
|
234
|
+
syncModelListener(el, state);
|
|
235
|
+
syncModelValue(el, value);
|
|
236
|
+
},
|
|
237
|
+
unmounted(el) {
|
|
238
|
+
modelStates.delete(el);
|
|
45
239
|
},
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
240
|
+
};
|
|
241
|
+
const SYSTEM_MODIFIERS = [
|
|
242
|
+
'ctrl',
|
|
243
|
+
'shift',
|
|
244
|
+
'alt',
|
|
245
|
+
'meta',
|
|
246
|
+
];
|
|
247
|
+
const MODIFIER_GUARDS = {
|
|
248
|
+
stop: event => {
|
|
249
|
+
event.stopPropagation?.();
|
|
250
|
+
return false;
|
|
251
|
+
},
|
|
252
|
+
prevent: event => {
|
|
253
|
+
event.preventDefault?.();
|
|
254
|
+
return false;
|
|
57
255
|
},
|
|
58
|
-
|
|
256
|
+
self: event => event.target !== event.currentTarget,
|
|
257
|
+
ctrl: event => !event.ctrlKey,
|
|
258
|
+
shift: event => !event.shiftKey,
|
|
259
|
+
alt: event => !event.altKey,
|
|
260
|
+
meta: event => !event.metaKey,
|
|
261
|
+
left: event => event.button !== 0,
|
|
262
|
+
middle: event => event.button !== 1,
|
|
263
|
+
right: event => event.button !== 2,
|
|
264
|
+
exact: (event, modifiers) => SYSTEM_MODIFIERS.some(modifier => event[`${modifier}Key`] && !modifiers.includes(modifier)),
|
|
265
|
+
};
|
|
266
|
+
/**
|
|
267
|
+
* Render-function/compiled-template equivalent of `v-on.stop`/`.prevent`/`.self`/etc. `fn` is
|
|
268
|
+
* typed as required, matching upstream Vue's own declaration — its runtime-only `!fn` guard below
|
|
269
|
+
* (for a compiler-generated call site that could pass a falsy handler) isn't reflected in the
|
|
270
|
+
* type there either.
|
|
271
|
+
*/
|
|
272
|
+
export function withModifiers(fn, modifiers) {
|
|
273
|
+
if (!fn)
|
|
274
|
+
return fn;
|
|
275
|
+
const cache = fn._withMods ?? (fn._withMods = {});
|
|
276
|
+
const cacheKey = modifiers.join('.');
|
|
277
|
+
return (cache[cacheKey] ??= ((event, ...args) => {
|
|
278
|
+
for (const modifier of modifiers) {
|
|
279
|
+
if (MODIFIER_GUARDS[modifier](event, modifiers))
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
return fn(event, ...args);
|
|
283
|
+
}));
|
|
284
|
+
}
|
|
285
|
+
const KEY_ALIASES = {
|
|
286
|
+
esc: 'escape',
|
|
287
|
+
space: ' ',
|
|
288
|
+
up: 'arrow-up',
|
|
289
|
+
left: 'arrow-left',
|
|
290
|
+
right: 'arrow-right',
|
|
291
|
+
down: 'arrow-down',
|
|
292
|
+
delete: 'backspace',
|
|
293
|
+
};
|
|
294
|
+
// \B (non-word-boundary), not a bare capture: without it, "Enter" hyphenates to "-enter" (a
|
|
295
|
+
// leading dash) instead of "enter", since the very first character is otherwise still a match.
|
|
296
|
+
function hyphenate(camelCase) {
|
|
297
|
+
return camelCase.replace(/\B([A-Z])/g, '-$1').toLowerCase();
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Render-function/compiled-template equivalent of `v-on.enter`/`.esc`/etc. No-ops on any event
|
|
301
|
+
* without a `.key` field (e.g. Symbiote's Pressable `onPress`) — same as upstream Vue's own
|
|
302
|
+
* behavior on a non-keyboard DOM event, not a Symbiote-specific gap. `TextInput`'s `onKeyPress`
|
|
303
|
+
* carries `nativeEvent.key`, so that is the applicable Symbiote event shape.
|
|
304
|
+
*/
|
|
305
|
+
export function withKeys(fn, modifiers) {
|
|
306
|
+
const cache = fn._withKeys ?? (fn._withKeys = {});
|
|
307
|
+
const cacheKey = modifiers.join('.');
|
|
308
|
+
return (cache[cacheKey] ??= (event) => {
|
|
309
|
+
if (event.key == null)
|
|
310
|
+
return undefined;
|
|
311
|
+
const eventKey = hyphenate(event.key);
|
|
312
|
+
if (modifiers.some(modifier => modifier === eventKey || KEY_ALIASES[modifier] === eventKey)) {
|
|
313
|
+
return fn(event);
|
|
314
|
+
}
|
|
315
|
+
return undefined;
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Reads the class map a `<style module>` SFC block compiles onto the component's own options
|
|
320
|
+
* (`__cssModules`, set by @symbiote-native/css-parser's SFC compiler — see the
|
|
321
|
+
* `symbiote-sfc-style-compiler` skill). Warns and returns an empty map exactly like upstream Vue
|
|
322
|
+
* does when the current component has no such block, rather than throwing.
|
|
323
|
+
*/
|
|
324
|
+
export function useCssModule(name = '$style') {
|
|
325
|
+
const instance = getCurrentInstance();
|
|
326
|
+
if (!instance) {
|
|
327
|
+
warn('useCssModule must be called inside setup()');
|
|
328
|
+
return {};
|
|
329
|
+
}
|
|
330
|
+
const modules = instance.type
|
|
331
|
+
.__cssModules;
|
|
332
|
+
if (!modules) {
|
|
333
|
+
warn('Current instance does not have CSS modules injected.');
|
|
334
|
+
return {};
|
|
335
|
+
}
|
|
336
|
+
const cssModule = modules[name];
|
|
337
|
+
if (!cssModule) {
|
|
338
|
+
warn(`Current instance does not have CSS module named "${name}".`);
|
|
339
|
+
return {};
|
|
340
|
+
}
|
|
341
|
+
return cssModule;
|
|
342
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { resolveStateStyle, type IPressStateArgument, type IResolvedStateStyle, } from '@symbiote-native/components/state-style';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Re-export only. The implementation is shared (`@symbiote-native/components`) because two adapters
|
|
2
|
+
// had written byte-identical copies; this file exists so the code a transform EMITS keeps importing
|
|
3
|
+
// from the adapter package the app already depends on, rather than reaching past it.
|
|
4
|
+
//
|
|
5
|
+
// Imported from the SUBPATH, not the barrel: every adapter barrel re-exports
|
|
6
|
+
// `@symbiote-native/components` wholesale, so a name on that barrel silently becomes public API on
|
|
7
|
+
// all five — which is how a transform internal nearly shipped as a supported export.
|
|
8
|
+
//
|
|
9
|
+
// Present on every adapter, including those with no lowering transform yet: a per-framework subpath
|
|
10
|
+
// that four of five declare is exactly the silent gap `tests/package-subpath-parity.test.ts` exists
|
|
11
|
+
// to catch, and the file costs a re-export. Deliberately NOT on the public barrel — the emitted
|
|
12
|
+
// code names it, app code does not.
|
|
13
|
+
export { resolveStateStyle, } from '@symbiote-native/components/state-style';
|
|
@@ -12,19 +12,27 @@ const KEBAB_SEGMENT = /-([a-z])/g;
|
|
|
12
12
|
function toCamel(key) {
|
|
13
13
|
return key.replace(KEBAB_SEGMENT, (_match, char) => char.toUpperCase());
|
|
14
14
|
}
|
|
15
|
+
// The per-KEY half, for the path that never sees a whole attrs bag: a host primitive lowered to
|
|
16
|
+
// its intrinsic tag by the SFC transformer reaches the renderer one patchProp call at a time,
|
|
17
|
+
// with no component in between to fold the bag. Returns the key unchanged (same string identity)
|
|
18
|
+
// whenever there is nothing to convert, which is every key on the hot path.
|
|
19
|
+
export function normalizeVueAttrKey(key) {
|
|
20
|
+
if (!key.includes('-'))
|
|
21
|
+
return key;
|
|
22
|
+
if (key.startsWith('aria-') || key.startsWith('data-'))
|
|
23
|
+
return key;
|
|
24
|
+
return toCamel(key);
|
|
25
|
+
}
|
|
26
|
+
// Always returns a PLAIN copy, never the input. An earlier version handed the input back when no
|
|
27
|
+
// key needed converting - which is every SFC/TSX bag, since `@press` / `:style` already arrive
|
|
28
|
+
// camel - and that gave callers Vue's attrs PROXY to read from. In production, not just dev, that
|
|
29
|
+
// proxy's get trap is `track(target, "get", "")` before every value
|
|
30
|
+
// (runtime-core.esm-bundler.js:8357), so each of Pressable's ~16 per-render attr reads paid two
|
|
31
|
+
// map lookups it did not need. The copy loop reads every key through the trap once regardless, so
|
|
32
|
+
// the only thing the early return ever saved was this one object.
|
|
15
33
|
export function normalizeVueAttrs(attrs) {
|
|
16
|
-
let changed = false;
|
|
17
34
|
const out = {};
|
|
18
|
-
for (const key of Object.keys(attrs))
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
changed = true;
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
out[key] = attrs[key];
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
// Return the original object when nothing converted (the already-camel / TSX path): avoids a
|
|
28
|
-
// per-render allocation and keeps attrs identity stable.
|
|
29
|
-
return changed ? out : attrs;
|
|
35
|
+
for (const key of Object.keys(attrs))
|
|
36
|
+
out[normalizeVueAttrKey(key)] = attrs[key];
|
|
37
|
+
return out;
|
|
30
38
|
}
|
package/metro-css-parser.cjs
CHANGED
|
@@ -9,4 +9,5 @@
|
|
|
9
9
|
// directly instead, since it needs the individual parse/compile functions, not a Metro transformer
|
|
10
10
|
// object. createCssMetroTransformer is a factory — babelTransformerPath needs the actual
|
|
11
11
|
// {transform, getCacheKey} object it returns, not the css-parser package barrel.
|
|
12
|
-
module.exports =
|
|
12
|
+
module.exports =
|
|
13
|
+
require('@symbiote-native/css-parser').createCssMetroTransformer();
|