@symbiote-native/vue 0.3.8 → 0.5.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 (46) hide show
  1. package/README.md +6 -6
  2. package/build/bootstrap.js +1 -1
  3. package/build/components/flat-list/index.js +32 -39
  4. package/build/components/image-background.js +8 -19
  5. package/build/components/image.js +12 -21
  6. package/build/components/keyboard-avoiding-view.js +47 -31
  7. package/build/components/modal/index.d.ts +4 -4
  8. package/build/components/modal/index.js +5 -2
  9. package/build/components/pressable.d.ts +1 -1
  10. package/build/components/pressable.js +25 -32
  11. package/build/components/refresh-control.js +12 -18
  12. package/build/components/scroll-view/index.android.d.ts +1 -1
  13. package/build/components/scroll-view/index.android.js +24 -34
  14. package/build/components/scroll-view/index.ios.d.ts +1 -1
  15. package/build/components/scroll-view/index.ios.js +3 -1
  16. package/build/components/scroll-view/shared.js +93 -123
  17. package/build/components/scroll-view/sticky-header.js +59 -64
  18. package/build/components/section-list/index.js +16 -21
  19. package/build/components/switch/shared.js +29 -32
  20. package/build/components/text-input/index.js +36 -51
  21. package/build/components/touchable-native-feedback.js +3 -1
  22. package/build/components/touchable.d.ts +7 -1
  23. package/build/components/touchable.js +181 -33
  24. package/build/components/virtualized-list/index.js +57 -18
  25. package/build/components/virtualized-section-list/index.d.ts +5 -0
  26. package/build/components/virtualized-section-list/index.js +50 -29
  27. package/build/components.js +9 -6
  28. package/build/create-portal/index.d.ts +28 -0
  29. package/build/create-portal/index.js +42 -0
  30. package/build/host-instance/index.js +5 -5
  31. package/build/index.d.ts +9 -6
  32. package/build/index.js +18 -46
  33. package/build/modules/animated/create-animated-component.js +46 -101
  34. package/build/modules/animated/index.d.ts +2 -0
  35. package/build/modules/animated/index.js +26 -17
  36. package/build/modules/app-registry/index.js +11 -13
  37. package/build/modules/status-bar.js +12 -17
  38. package/build/render.d.ts +3 -1
  39. package/build/render.js +62 -1
  40. package/build/renderer/index.js +11 -6
  41. package/build/runtime-helpers/index.d.ts +44 -24
  42. package/build/runtime-helpers/index.js +116 -47
  43. package/build/utils/normalize-attrs.js +3 -1
  44. package/metro-css-parser.cjs +9 -14
  45. package/metro-vue-transformer.cjs +196 -178
  46. package/package.json +5 -5
@@ -1,27 +1,24 @@
1
1
  // StatusBar, the Vue lifecycle half. The native StatusBarManager driving (applyStatusBarProps),
2
- // the imperative statics (statusBarImperative), and the Android bar-height constant all live in
3
- // @symbiote-native/engine, shared verbatim with React; Metro selects the engine's status-bar.ios.ts /
4
- // status-bar.android.ts per host, so the platform divergence never reaches this file. Vue supplies
5
- // only the declarative shape: a component that renders NOTHING and re-applies the props through a
6
- // watchEffect on mount + every prop change, with the imperative statics attached to the component
7
- // object (RN's StatusBar: the value doubles as the imperative namespace, like the Vue Image).
2
+ // the imperative statics, and the Android bar-height constant all live in @symbiote-native/engine,
3
+ // shared verbatim with React; Metro selects the engine's status-bar.ios.ts/.android.ts per host,
4
+ // so platform divergence never reaches this file. Vue supplies only the declarative shape: a
5
+ // component that renders NOTHING and re-applies the props through a watchEffect on mount + every
6
+ // prop change, with the imperative statics attached to the component object.
8
7
  //
9
- // Renders null (no Fabric view), so it follows neither the descriptor split nor a host-node ref:
10
- // the "else keep its current shape" path of the StatusBar component. Inputs arrive as attrs
11
- // (untyped) and run through normalizeVueAttrs (kebab→camel) before each is narrowed by a guard.
12
- import { defineComponent, watchEffect } from '@vue/runtime-core';
8
+ // Inputs arrive as attrs (untyped) and run through normalizeVueAttrs before each is narrowed by a guard.
9
+ import { defineComponent, watchEffect, } from '@vue/runtime-core';
13
10
  import { applyStatusBarProps, statusBarImperative, statusBarCurrentHeight, isOpaqueColorValue, } from '@symbiote-native/engine';
14
11
  import { normalizeVueAttrs } from '../utils/normalize-attrs.js';
15
12
  function asBoolean(value) {
16
13
  return typeof value === 'boolean' ? value : undefined;
17
14
  }
18
15
  function asBarStyle(value) {
19
- return value === 'default' || value === 'light-content' || value === 'dark-content'
16
+ return value === 'default' ||
17
+ value === 'light-content' ||
18
+ value === 'dark-content'
20
19
  ? value
21
20
  : undefined;
22
21
  }
23
- // backgroundColor is a string / int / opaque PlatformColor; any other shape is dropped. A runtime
24
- // guard at the untyped-attrs boundary, not a cast.
25
22
  function asColorValue(value) {
26
23
  if (typeof value === 'string')
27
24
  return value;
@@ -43,13 +40,11 @@ const StatusBarComponent = defineComponent({
43
40
  name: 'StatusBar',
44
41
  inheritAttrs: false,
45
42
  setup(_props, { attrs: rawAttrs }) {
46
- // watchEffect tracks rawAttrs (reactive), so reading it here re-applies on every prop change:
47
- // the Vue twin of React's useEffect over the prop deps. Resolution is lazy inside the engine
48
- // (a missing StatusBarManager is a no-op), so this never crashes a render.
43
+ // watchEffect tracks rawAttrs (reactive), so reading it here re-applies on every prop change.
44
+ // Resolution is lazy inside the engine (a missing StatusBarManager is a no-op).
49
45
  watchEffect(() => {
50
46
  applyStatusBarProps(buildProps(normalizeVueAttrs(rawAttrs)));
51
47
  });
52
- // Renders nothing: StatusBar has no Fabric view, exactly like React's `return null`.
53
48
  return () => null;
54
49
  },
55
50
  });
package/build/render.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { type IRootTag, type SymbioteSurface } from '@symbiote-native/engine';
2
- import type { Component } from '@vue/runtime-core';
2
+ import type { App, Component } from '@vue/runtime-core';
3
+ export type IAppConfigurator = (app: App) => void;
4
+ export declare function setAppConfigurator(configure: IAppConfigurator | undefined): void;
3
5
  export declare function mount(rootTag: IRootTag, rootComponent: Component): SymbioteSurface;
4
6
  export declare function unmount(rootTag: IRootTag): void;
5
7
  declare global {
package/build/render.js CHANGED
@@ -1,12 +1,71 @@
1
1
  // Mount a Vue app onto a Fabric surface. The native host hands us a rootTag; we create
2
2
  // a surface for it and let the Vue renderer drive the engine, which commits into
3
3
  // nativeFabricUIManager; RN's own renderer never in the path.
4
- import { createSurface, disposeRoot, dlog, } from '@symbiote-native/engine';
4
+ import { createSurface, disposeRoot, dlog, reportUncaughtError, } from '@symbiote-native/engine';
5
5
  import { createSymbioteRenderer } from './renderer/index.js';
6
6
  // One Vue app per surface, so a surface can be torn down (unmount) or cleanly
7
7
  // re-mounted on the same rootTag: the bridgeless host stops and restarts a surface on
8
8
  // Fast Refresh and on lifecycle/focus changes, reusing the rootTag.
9
9
  const apps = new Map();
10
+ // A stack this deep already names the screen; the rest is noise in a redbox.
11
+ const MAX_STACK_FRAMES = 20;
12
+ // SFC components carry their name on `__name` (the compiler's), plain ones on `name`; neither is
13
+ // on Vue's public `ConcreteComponent` type, hence the guarded read rather than a cast.
14
+ function displayName(type) {
15
+ for (const key of ['name', '__name']) {
16
+ const value = Reflect.get(type, key);
17
+ if (typeof value === 'string' && value.length > 0)
18
+ return value;
19
+ }
20
+ return 'Anonymous';
21
+ }
22
+ // Vue's own component trace (getComponentTrace, used by `warn`) is internal, so the frames are
23
+ // walked off the public instance's parent chain. LogBox renders whatever string it is handed;
24
+ // React's shape is `\n in <Name>`, and matching it keeps the redbox reading the same in an
25
+ // app that mixes adapters' errors in one console.
26
+ function componentStack(instance) {
27
+ if (instance === null)
28
+ return null;
29
+ const frames = [];
30
+ let current = instance.$;
31
+ while (current !== null && frames.length < MAX_STACK_FRAMES) {
32
+ frames.push(`\n in ${displayName(current.type)}`);
33
+ current = current.parent;
34
+ }
35
+ return frames.length === 0 ? null : frames.join('');
36
+ }
37
+ // The app-level error seam. Without it, an unclaimed error fell through to Vue's own `logError`
38
+ // (runtime-core errorHandling.ts), which does neither useful thing: in a dev bundle it RE-THROWS
39
+ // out of `app.mount()`, so it escapes this function, aborts the AppRegistry runnable and leaves
40
+ // the surface half-brought-up; in a release bundle it console.errors a bare error with no origin
41
+ // and no component context, never touching `global.ErrorUtils`, so nothing reaches the host.
42
+ // Setting a handler suppresses both branches and routes to the engine's shared channel instead —
43
+ // the native redbox on a device, console.error off one.
44
+ //
45
+ // CAUGHT vs UNCAUGHT — the asymmetry is Vue's own, and it costs us no code. `handleError` walks
46
+ // the `onErrorCaptured` chain BEFORE consulting `config.errorHandler`, and a hook returning
47
+ // `false` stops propagation and returns early (runtime-core 3.5 errorHandling.ts), so a boundary
48
+ // that claims the error never reaches this function at all. That is the behaviour we want and
49
+ // the same split the React adapter makes by hand: writing a boundary IS the developer saying
50
+ // "I am handling this", and answering it with a full-screen redbox over the fallback the app
51
+ // just rendered contradicts what the app asked for.
52
+ //
53
+ // So do NOT "fix" this to report from inside an `onErrorCaptured` of our own — that would
54
+ // re-report exactly the errors a boundary claimed. A hook that returns undefined instead of
55
+ // `false` still lands here, and correctly: Vue treats that error as still propagating.
56
+ function reportToHost(error, instance, info) {
57
+ // `info` is Vue's own phase label ('render function', 'setup function', 'native event
58
+ // handler'), which is the closest thing it has to React's "no error boundary" / "recovered"
59
+ // qualifier. Off a native host it prefixes the log line, where there is no redbox to carry it.
60
+ reportUncaughtError(error, {
61
+ origin: `vue render (${String(info)})`,
62
+ componentStack: componentStack(instance),
63
+ });
64
+ }
65
+ let configurator;
66
+ export function setAppConfigurator(configure) {
67
+ configurator = configure;
68
+ }
10
69
  function teardown(rootTag) {
11
70
  const app = apps.get(rootTag);
12
71
  if (app === undefined)
@@ -22,6 +81,8 @@ export function mount(rootTag, rootComponent) {
22
81
  const surface = createSurface(rootTag);
23
82
  const renderer = createSymbioteRenderer(surface);
24
83
  const app = renderer.createApp(rootComponent);
84
+ app.config.errorHandler = reportToHost;
85
+ configurator?.(app);
25
86
  apps.set(rootTag, app);
26
87
  // The surface IS the Vue container: a top-level mutation routes to surface.appendChild
27
88
  // (renderer.ts), and the engine wraps surface.children in its synthetic flex root.
@@ -51,6 +51,13 @@ export function createSymbioteRenderer(surface) {
51
51
  setElementText(el, text) {
52
52
  if (isSurface(el))
53
53
  return;
54
+ // Same invariant insert() enforces, on the other route text can reach a node: Vue calls
55
+ // this instead of insert() when an element's children collapse to a single string, so
56
+ // without the check a raw text lands under a non-Text parent - an invalid Fabric tree
57
+ // built silently, which is worse than the throw insert() would have given.
58
+ if (!el.isText) {
59
+ throw new Error(`Text string "${text}" must be rendered inside a <Text>`);
60
+ }
54
61
  // An RCTText carries its string as a single raw-text child. Reuse a lone existing
55
62
  // one to avoid churn; otherwise replace all children with a fresh raw-text node.
56
63
  const [first] = el.children;
@@ -103,12 +110,10 @@ export function createSymbioteRenderer(surface) {
103
110
  patchProp(el, key, _prev, next) {
104
111
  if (isSurface(el))
105
112
  return;
106
- // routeProp makes the prop-vs-event decision from the node's ViewConfig (onPress on
107
- // a View becomes a listener; onTintColor on a Switch stays a prop): identical to
108
- // the React host config, so the whole event layer is shared. `class`/`style` merge
109
- // (explicit :style always winning over class-derived style, regardless of which of
110
- // Vue's two independent patchProp calls lands last) is centralized in routeProp too
111
- // (core/engine/src/node.ts) — every adapter gets it, not just this one.
113
+ // routeProp makes the prop-vs-event decision from the node's ViewConfig (onPress on a
114
+ // View becomes a listener; onTintColor on a Switch stays a prop), shared with React. The
115
+ // class/style merge (explicit :style always winning, regardless of which of Vue's two
116
+ // independent patchProp calls lands last) is centralized there too (core/engine/src/node.ts).
112
117
  routeProp(el, key, next);
113
118
  surface.requestCommit();
114
119
  },
@@ -1,28 +1,48 @@
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';
4
5
  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>;
6
+ type ISystemModifier = 'ctrl' | 'shift' | 'alt' | 'meta';
7
+ type IEventModifier = ISystemModifier | 'stop' | 'prevent' | 'self' | 'left' | 'middle' | 'right' | 'exact';
8
+ type IModifierGuardableEvent = {
9
+ stopPropagation?: () => void;
10
+ preventDefault?: () => void;
11
+ target?: unknown;
12
+ currentTarget?: unknown;
13
+ button?: number;
14
+ ctrlKey?: boolean;
15
+ shiftKey?: boolean;
16
+ altKey?: boolean;
17
+ metaKey?: boolean;
18
+ };
19
+ type IEventHandler<TEvent> = ((event: TEvent, ...args: never[]) => unknown) & {
20
+ _withMods?: Record<string, IEventHandler<TEvent>>;
21
+ };
22
+ /**
23
+ * Render-function/compiled-template equivalent of `v-on.stop`/`.prevent`/`.self`/etc. `fn` is
24
+ * typed as required, matching upstream Vue's own declaration — its runtime-only `!fn` guard below
25
+ * (for a compiler-generated call site that could pass a falsy handler) isn't reflected in the
26
+ * type there either.
27
+ */
28
+ export declare function withModifiers<TEvent extends IModifierGuardableEvent>(fn: IEventHandler<TEvent>, modifiers: readonly IEventModifier[]): IEventHandler<TEvent>;
29
+ type IKeyedEvent = {
30
+ key?: string;
31
+ };
32
+ type IKeyedHandler<TEvent extends IKeyedEvent> = ((event: TEvent) => unknown) & {
33
+ _withKeys?: Record<string, (event: TEvent) => unknown>;
34
+ };
35
+ /**
36
+ * Render-function/compiled-template equivalent of `v-on.enter`/`.esc`/etc. No-ops on any event
37
+ * without a `.key` field (e.g. Symbiote's Pressable `onPress`) — same as upstream Vue's own
38
+ * behavior on a non-keyboard DOM event, not a Symbiote-specific gap. `TextInput`'s `onKeyPress`
39
+ * carries `nativeEvent.key`, so that is the applicable Symbiote event shape.
40
+ */
41
+ export declare function withKeys<TEvent extends IKeyedEvent>(fn: IKeyedHandler<TEvent>, modifiers: readonly string[]): (event: TEvent) => unknown;
42
+ /**
43
+ * Reads the class map a `<style module>` SFC block compiles onto the component's own options
44
+ * (`__cssModules`, set by @symbiote-native/css-parser's SFC compiler — see the
45
+ * `symbiote-sfc-style-compiler` skill). Warns and returns an empty map exactly like upstream Vue
46
+ * does when the current component has no such block, rather than throwing.
47
+ */
48
+ export declare function useCssModule(name?: string): Record<string, string>;
@@ -1,28 +1,26 @@
1
1
  // Metro rewrites every compiled `from 'vue'` import to point HERE instead of at bare
2
- // `@vue/runtime-core` (see examples/*/metro-vue-transformer.js). Most of the compiler's injected
3
- // helpers (ref, computed, withDirectives, openBlock, …) already live in runtime-core untouched —
4
- // re-exported below. But two Vue template directives compile to a runtime-helper import that
5
- // ONLY exists in `@vue/runtime-dom` (vShow, vModelText, vModelCheckbox, vModelSelect), written
6
- // directly against a real HTMLElement (el.style.display, el.value, addEventListener). We have no
7
- // DOM, so those don't exist for us — rewriting straight to runtime-core leaves `v-show` either
8
- // failing at import time or silently resolving to `undefined` (a no-op directive). This module is
9
- // the interception point: it supplies our OWN implementation under the same export name, so the
10
- // compiler's import keeps resolving and the directive actually does something.
2
+ // @vue/runtime-core (see examples/*/metro-vue-transformer.js). Most compiler-injected helpers
3
+ // (ref, computed, withDirectives, openBlock, ...) live in runtime-core untouched, re-exported
4
+ // below. But two Vue template directives compile to a runtime-helper import that ONLY exists in
5
+ // @vue/runtime-dom (vShow, vModelText, vModelCheckbox, vModelSelect), written directly against a
6
+ // real HTMLElement - we have no DOM, so this module supplies our OWN implementation under the
7
+ // same export name instead of leaving `v-show` a silent no-op.
11
8
  //
12
9
  // Scope: this covers only Vue's own template directives (v-show, Teleport); native-element
13
10
  // v-model is a separate, out-of-scope case.
14
11
  export * from '@vue/runtime-core';
15
- import { defineComponent, h, Teleport as VueTeleport, } from '@vue/runtime-core';
16
- import { isSymbioteNode, setNativeProps, whenCommitted, } from '@symbiote-native/engine';
17
- // setNativeProps merges a partial `style` object onto the node's current one (never clobbering
18
- // other declarative style fields) and re-commits via the node's OWN tracked rootTag — no
19
- // SymbioteSurface reference needed here, which a directive hook has no access to anyway.
20
- //
12
+ import { getCurrentInstance, warn, } from '@vue/runtime-core';
13
+ import { setNativeProps, whenCommitted, } from '@symbiote-native/engine';
14
+ // Shadows runtime-core's own Teleport for every compiled `from 'vue'` import (see the header):
15
+ // same component, plus a runtime guard on `to`. Lives in ../create-portal so the adapter's portal
16
+ // sits where every other adapter's does; re-exported here because THIS is the module Metro
17
+ // rewrites `vue` to, and an `export *` alone would resolve `Teleport` to the unguarded original.
18
+ export { Teleport } from '../create-portal/index.js';
21
19
  // whenCommitted, not a direct call: Vue's `mounted` hook fires synchronously during the patch
22
20
  // pass, but this renderer coalesces the actual Fabric commit onto a microtask
23
- // (surface.requestCommit()), so the node may have no committed tag yet — the same async-commit
24
- // race TextInput's autoFocus guards against. A bare setNativeProps
25
- // here would silently no-op with no retry on the very first mount.
21
+ // (surface.requestCommit()), so the node may have no committed tag yet - the same async-commit
22
+ // race TextInput's autoFocus guards against. A bare setNativeProps here would silently no-op
23
+ // with no retry on the very first mount.
26
24
  const pendingShowCommits = new WeakMap();
27
25
  function applyShow(el, value) {
28
26
  pendingShowCommits.get(el)?.();
@@ -35,34 +33,105 @@ export const vShow = {
35
33
  // Drop a still-pending first-commit wait if the element unmounts before it ever lands.
36
34
  unmounted: el => pendingShowCommits.get(el)?.(),
37
35
  };
38
- // Teleport's `to` in the DOM world is usually a CSS-selector string ('body', '#modal-root'); we
39
- // have no querySelector (renderer.ts stubs it to null, same as insertStaticContent), so `to` here
40
- // must be an already-mounted host node — e.g. a ref to a persistent "overlay host" View rendered
41
- // once near the app root — not a selector string. This wrapper shadows runtime-core's own
42
- // `Teleport` (which the SFC compiler imports from 'vue' like vShow) purely to validate `to` BEFORE
43
- // handing it to the real Teleport: a wrong value (string, plain object, an unrendered/reactive-
44
- // wrapped node) throws immediately here instead of silently corrupting the retained tree deep
45
- // inside insert/remove — the "don't let a dev accidentally break everything" case this exists for.
46
- // Scope: same-surface targets only (see the React createPortal twin, create-portal.ts) — Vue's own
47
- // Teleport internals (disabled toggling, move-on-update) work unmodified once `to` resolves to a
48
- // real ISymbioteNode, since insert/remove/parentNode/nextSibling are already fully generic.
49
- export const Teleport = defineComponent({
50
- name: 'Teleport',
51
- inheritAttrs: false,
52
- props: {
53
- to: { type: null, default: null },
54
- disabled: { type: Boolean, default: false },
36
+ const SYSTEM_MODIFIERS = [
37
+ 'ctrl',
38
+ 'shift',
39
+ 'alt',
40
+ 'meta',
41
+ ];
42
+ const MODIFIER_GUARDS = {
43
+ stop: event => {
44
+ event.stopPropagation?.();
45
+ return false;
55
46
  },
56
- setup(props, { slots }) {
57
- return () => {
58
- const { to } = props;
59
- if (typeof to === 'string') {
60
- 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").`);
61
- }
62
- if (to != null && !isSymbioteNode(to)) {
63
- throw new Error('Teleport target is not a real host node — did you forget `.value` on a ref, or pass something symbiote never rendered?');
64
- }
65
- return h(VueTeleport, { to, disabled: props.disabled }, slots);
66
- };
47
+ prevent: event => {
48
+ event.preventDefault?.();
49
+ return false;
67
50
  },
68
- });
51
+ self: event => event.target !== event.currentTarget,
52
+ ctrl: event => !event.ctrlKey,
53
+ shift: event => !event.shiftKey,
54
+ alt: event => !event.altKey,
55
+ meta: event => !event.metaKey,
56
+ left: event => event.button !== 0,
57
+ middle: event => event.button !== 1,
58
+ right: event => event.button !== 2,
59
+ exact: (event, modifiers) => SYSTEM_MODIFIERS.some(modifier => event[`${modifier}Key`] && !modifiers.includes(modifier)),
60
+ };
61
+ /**
62
+ * Render-function/compiled-template equivalent of `v-on.stop`/`.prevent`/`.self`/etc. `fn` is
63
+ * typed as required, matching upstream Vue's own declaration — its runtime-only `!fn` guard below
64
+ * (for a compiler-generated call site that could pass a falsy handler) isn't reflected in the
65
+ * type there either.
66
+ */
67
+ export function withModifiers(fn, modifiers) {
68
+ if (!fn)
69
+ return fn;
70
+ const cache = fn._withMods ?? (fn._withMods = {});
71
+ const cacheKey = modifiers.join('.');
72
+ return (cache[cacheKey] ??= ((event, ...args) => {
73
+ for (const modifier of modifiers) {
74
+ if (MODIFIER_GUARDS[modifier](event, modifiers))
75
+ return undefined;
76
+ }
77
+ return fn(event, ...args);
78
+ }));
79
+ }
80
+ const KEY_ALIASES = {
81
+ esc: 'escape',
82
+ space: ' ',
83
+ up: 'arrow-up',
84
+ left: 'arrow-left',
85
+ right: 'arrow-right',
86
+ down: 'arrow-down',
87
+ delete: 'backspace',
88
+ };
89
+ // \B (non-word-boundary), not a bare capture: without it, "Enter" hyphenates to "-enter" (a
90
+ // leading dash) instead of "enter", since the very first character is otherwise still a match.
91
+ function hyphenate(camelCase) {
92
+ return camelCase.replace(/\B([A-Z])/g, '-$1').toLowerCase();
93
+ }
94
+ /**
95
+ * Render-function/compiled-template equivalent of `v-on.enter`/`.esc`/etc. No-ops on any event
96
+ * without a `.key` field (e.g. Symbiote's Pressable `onPress`) — same as upstream Vue's own
97
+ * behavior on a non-keyboard DOM event, not a Symbiote-specific gap. `TextInput`'s `onKeyPress`
98
+ * carries `nativeEvent.key`, so that is the applicable Symbiote event shape.
99
+ */
100
+ export function withKeys(fn, modifiers) {
101
+ const cache = fn._withKeys ?? (fn._withKeys = {});
102
+ const cacheKey = modifiers.join('.');
103
+ return (cache[cacheKey] ??= (event) => {
104
+ if (event.key == null)
105
+ return undefined;
106
+ const eventKey = hyphenate(event.key);
107
+ if (modifiers.some(modifier => modifier === eventKey || KEY_ALIASES[modifier] === eventKey)) {
108
+ return fn(event);
109
+ }
110
+ return undefined;
111
+ });
112
+ }
113
+ /**
114
+ * Reads the class map a `<style module>` SFC block compiles onto the component's own options
115
+ * (`__cssModules`, set by @symbiote-native/css-parser's SFC compiler — see the
116
+ * `symbiote-sfc-style-compiler` skill). Warns and returns an empty map exactly like upstream Vue
117
+ * does when the current component has no such block, rather than throwing.
118
+ */
119
+ export function useCssModule(name = '$style') {
120
+ const instance = getCurrentInstance();
121
+ if (!instance) {
122
+ warn('useCssModule must be called inside setup()');
123
+ return {};
124
+ }
125
+ const modules = instance.type
126
+ .__cssModules;
127
+ if (!modules) {
128
+ warn('Current instance does not have CSS modules injected.');
129
+ return {};
130
+ }
131
+ const cssModule = modules[name];
132
+ if (!cssModule) {
133
+ warn(`Current instance does not have CSS module named "${name}".`);
134
+ return {};
135
+ }
136
+ return cssModule;
137
+ }
@@ -16,7 +16,9 @@ export function normalizeVueAttrs(attrs) {
16
16
  let changed = false;
17
17
  const out = {};
18
18
  for (const key of Object.keys(attrs)) {
19
- if (key.includes('-') && !key.startsWith('aria-') && !key.startsWith('data-')) {
19
+ if (key.includes('-') &&
20
+ !key.startsWith('aria-') &&
21
+ !key.startsWith('data-')) {
20
22
  out[toCamel(key)] = attrs[key];
21
23
  changed = true;
22
24
  }
@@ -1,17 +1,12 @@
1
1
  // Re-exports @symbiote-native/css-parser verbatim from INSIDE this package, so a consuming app's own
2
2
  // require('@symbiote-native/vue/metro-css-parser') resolves css-parser relative to THIS file's
3
- // location (adapters/vue/node_modules, where css-parser IS a real dependency — see
4
- // package.json), not relative to the app's own node_modules, which does not and should not
5
- // need to declare @symbiote-native/css-parser itself. Node resolves each require() relative to the
6
- // requiring FILE's own location, so this indirection is what actually removes the extra install
7
- // step, not pnpm hoisting (which does not propagate this transitively across workspace packages
8
- // the way a flat classic node_modules would). .cjs, not .js: this package is "type": "module",
9
- // and Metro's babelTransformerPath loading expects a require()-able module. Used as
10
- // vue-tsx's babelTransformerPath for standalone .css/.module.css file imports —
11
- // metro-vue-transformer.cjs (the .vue SFC transformer) requires @symbiote-native/css-parser
12
- // directly instead, since it needs the individual parse/compile functions, not a Metro
13
- // transformer object.
14
- // createCssMetroTransformer is a factory, not a ready transformer — Metro's
15
- // babelTransformerPath needs the actual {transform, getCacheKey} object it returns,
16
- // not the css-parser package barrel.
3
+ // location (adapters/vue/node_modules, where css-parser IS a real dependency), not relative to
4
+ // the app's own node_modules — Node resolves each require() relative to the requiring FILE, so
5
+ // this indirection is what removes the app's extra install step, not pnpm hoisting. .cjs, not
6
+ // .js: this package is "type": "module", and Metro's babelTransformerPath loading expects a
7
+ // require()-able module. Used as vue-tsx's babelTransformerPath for standalone .css/.module.css
8
+ // imports; metro-vue-transformer.cjs (the .vue SFC transformer) requires @symbiote-native/css-parser
9
+ // directly instead, since it needs the individual parse/compile functions, not a Metro transformer
10
+ // object. createCssMetroTransformer is a factory — babelTransformerPath needs the actual
11
+ // {transform, getCacheKey} object it returns, not the css-parser package barrel.
17
12
  module.exports = require('@symbiote-native/css-parser').createCssMetroTransformer();