@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.
Files changed (57) hide show
  1. package/README.md +24 -9
  2. package/babel-jsx.cjs +53 -0
  3. package/babel-lower-host-primitives.cjs +225 -0
  4. package/build/bootstrap.js +1 -1
  5. package/build/components/button.js +4 -2
  6. package/build/components/flat-list/index.js +14 -5
  7. package/build/components/image-background.js +4 -5
  8. package/build/components/image.js +3 -1
  9. package/build/components/keyboard-avoiding-view.js +39 -14
  10. package/build/components/modal/index.js +5 -2
  11. package/build/components/pressable.d.ts +1 -1
  12. package/build/components/pressable.js +39 -20
  13. package/build/components/safe-area-view.d.ts +1 -0
  14. package/build/components/scroll-view/index.android.d.ts +1 -1
  15. package/build/components/scroll-view/index.android.js +3 -1
  16. package/build/components/scroll-view/index.ios.d.ts +1 -1
  17. package/build/components/scroll-view/index.ios.js +3 -1
  18. package/build/components/scroll-view/shared.js +33 -13
  19. package/build/components/scroll-view/sticky-header.js +18 -5
  20. package/build/components/section-list/index.js +4 -2
  21. package/build/components/switch/shared.js +12 -3
  22. package/build/components/text-input/index.js +25 -2
  23. package/build/components/touchable-native-feedback.js +11 -4
  24. package/build/components/touchable.d.ts +7 -1
  25. package/build/components/touchable.js +184 -33
  26. package/build/components/virtualized-list/index.js +42 -20
  27. package/build/components/virtualized-section-list/index.d.ts +5 -0
  28. package/build/components/virtualized-section-list/index.js +33 -4
  29. package/build/components.d.ts +2 -0
  30. package/build/components.js +25 -4
  31. package/build/composables/use-raw-attrs.d.ts +1 -0
  32. package/build/composables/use-raw-attrs.js +26 -0
  33. package/build/create-portal/index.d.ts +28 -0
  34. package/build/create-portal/index.js +42 -0
  35. package/build/host-instance/index.js +7 -6
  36. package/build/index.d.ts +9 -6
  37. package/build/index.js +14 -3
  38. package/build/modules/animated/create-animated-component.d.ts +1 -1
  39. package/build/modules/animated/create-animated-component.js +27 -5
  40. package/build/modules/animated/index.d.ts +2 -0
  41. package/build/modules/animated/index.js +18 -5
  42. package/build/modules/app-registry/index.js +4 -2
  43. package/build/modules/status-bar.js +4 -2
  44. package/build/register.d.ts +1 -0
  45. package/build/register.js +35 -0
  46. package/build/render.d.ts +3 -1
  47. package/build/render.js +62 -1
  48. package/build/renderer/index.js +58 -9
  49. package/build/runtime-helpers/index.d.ts +48 -24
  50. package/build/runtime-helpers/index.js +314 -30
  51. package/build/state-style.d.ts +1 -0
  52. package/build/state-style.js +13 -0
  53. package/build/utils/normalize-attrs.d.ts +1 -0
  54. package/build/utils/normalize-attrs.js +21 -13
  55. package/metro-css-parser.cjs +2 -1
  56. package/metro-vue-transformer.cjs +502 -101
  57. 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 Teleport: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
6
- to: {
7
- type: null;
8
- default: null;
9
- };
10
- disabled: {
11
- type: BooleanConstructor;
12
- default: boolean;
13
- };
14
- }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
15
- [key: string]: any;
16
- }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
17
- to: {
18
- type: null;
19
- default: null;
20
- };
21
- disabled: {
22
- type: BooleanConstructor;
23
- default: boolean;
24
- };
25
- }>> & Readonly<{}>, {
26
- disabled: boolean;
27
- to: any;
28
- }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
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: this covers only Vue's own template directives (v-show, Teleport); native-element
10
- // v-model is a separate, out-of-scope case.
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 { defineComponent, h, Teleport as VueTeleport, } from '@vue/runtime-core';
13
- import { isSymbioteNode, setNativeProps, whenCommitted, } from '@symbiote-native/engine';
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
- // Teleport's `to` in the DOM world is usually a CSS-selector string ('body', '#modal-root'); we
32
- // have no querySelector, so `to` here must be an already-mounted host node - e.g. a ref to a
33
- // persistent "overlay host" View rendered once near the app root. This wrapper shadows
34
- // runtime-core's own Teleport purely to validate `to` BEFORE handing it to the real Teleport: a
35
- // wrong value throws immediately here instead of silently corrupting the retained tree deep
36
- // inside insert/remove. Scope: same-surface targets only (see the React createPortal twin,
37
- // create-portal.ts) - Vue's own Teleport internals work unmodified once `to` resolves to a real
38
- // ISymbioteNode.
39
- export const Teleport = defineComponent({
40
- name: 'Teleport',
41
- inheritAttrs: false,
42
- props: {
43
- to: { type: null, default: null },
44
- disabled: { type: Boolean, default: false },
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
- setup(props, { slots }) {
47
- return () => {
48
- const { to } = props;
49
- if (typeof to === 'string') {
50
- throw new Error(`Teleport target must be a host node ref, not a CSS-selector string ("${to}") — symbiote has no querySelector. Pass a ref to an already-rendered element instead (e.g. <View ref="overlayHost" />, then :to="overlayHost").`);
51
- }
52
- if (to != null && !isSymbioteNode(to)) {
53
- throw new Error('Teleport target is not a real host node — did you forget `.value` on a ref, or pass something symbiote never rendered?');
54
- }
55
- return h(VueTeleport, { to, disabled: props.disabled }, slots);
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';
@@ -1 +1,2 @@
1
+ export declare function normalizeVueAttrKey(key: string): string;
1
2
  export declare function normalizeVueAttrs(attrs: Record<string, unknown>): Record<string, unknown>;
@@ -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
- if (key.includes('-') && !key.startsWith('aria-') && !key.startsWith('data-')) {
20
- out[toCamel(key)] = attrs[key];
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
  }
@@ -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 = require('@symbiote-native/css-parser').createCssMetroTransformer();
12
+ module.exports =
13
+ require('@symbiote-native/css-parser').createCssMetroTransformer();