@symbiote-native/svelte 0.2.1 → 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/build/bootstrap.js +1 -1
- package/build/class-value.js +1 -1
- package/build/components/RefreshControl.svelte +4 -2
- package/build/components/Text.svelte +11 -2
- package/build/components/View.svelte +3 -1
- package/build/components/activity-indicator/index.svelte +8 -2
- package/build/components/button.svelte +8 -3
- package/build/components/flat-list/index.svelte +46 -11
- package/build/components/image/image-logic.js +13 -3
- package/build/components/image-background/index.svelte +8 -2
- package/build/components/index.d.ts +2 -2
- package/build/components/input-accessory-view/index.svelte +4 -1
- package/build/components/keyboard-avoiding-view/index.svelte +34 -11
- package/build/components/modal/index.svelte +4 -1
- package/build/components/pressable/index.svelte +48 -23
- package/build/components/pressable/index.svelte.d.ts +5 -1
- package/build/components/pressable/pressable-props.d.ts +1 -1
- package/build/components/safe-area-view-props.d.ts +1 -0
- package/build/components/scroll-view/index.svelte +54 -21
- package/build/components/scroll-view/sticky-header.svelte +49 -16
- package/build/components/section-list/index.svelte +6 -1
- package/build/components/switch/index.svelte +16 -5
- package/build/components/text-input/index.svelte +80 -8
- package/build/components/text-input/index.svelte.d.ts +5 -0
- package/build/components/touchable-highlight/index.svelte +115 -15
- package/build/components/touchable-highlight/touchable-highlight-props.d.ts +2 -0
- package/build/components/touchable-native-feedback/touchable-native-feedback.svelte +8 -3
- package/build/components/touchable-opacity/index.svelte +113 -56
- package/build/components/touchable-without-feedback/index.svelte +60 -4
- package/build/components/virtualized-list/index.svelte +233 -70
- package/build/components/virtualized-list/virtualized-list-props.js +8 -4
- package/build/components/virtualized-section-list/index.svelte +56 -9
- package/build/components/virtualized-section-list/index.svelte.d.ts +1 -1
- package/build/components/virtualized-section-list/virtualized-section-list-props.d.ts +5 -0
- package/build/create-portal/index.d.ts +22 -0
- package/build/create-portal/index.js +160 -0
- package/build/descriptor-to-svelte.js +15 -21
- package/build/dom-shim/element.d.ts +9 -3
- package/build/dom-shim/element.js +77 -25
- package/build/dom-shim/fold-host-bag.d.ts +1 -0
- package/build/dom-shim/fold-host-bag.js +7 -0
- package/build/dom-shim/index.d.ts +1 -0
- package/build/dom-shim/index.js +3 -0
- package/build/dom-shim/patch-globals.js +4 -2
- package/build/dom-shim/shim-node.js +11 -0
- package/build/dom-shim/text.js +38 -1
- package/build/host-instance.js +9 -5
- package/build/index.d.ts +6 -3
- package/build/index.js +20 -11
- package/build/modules/animated/create-animated-component.d.ts +5 -0
- package/build/modules/animated/create-animated-component.js +131 -0
- package/build/modules/animated/index.d.ts +24 -14
- package/build/modules/animated/index.js +30 -30
- package/build/modules/status-bar/index.js +1 -1
- package/build/modules/status-bar/index.svelte +4 -1
- package/build/native-view-bridge.d.ts +1 -1
- package/build/native-view-bridge.js +1 -1
- package/build/preprocessor/collapse-text-whitespace.js +20 -7
- package/build/preprocessor/forbid-web-only-constructs.js +8 -2
- package/build/preprocessor/lower-host-primitives.d.ts +10 -0
- package/build/preprocessor/lower-host-primitives.js +455 -0
- package/build/preprocessor/scoped-styles.js +141 -104
- package/build/register.d.ts +1 -0
- package/build/register.js +33 -0
- package/build/render.js +48 -3
- package/build/scope-token.d.ts +3 -2
- package/build/scope-token.js +6 -17
- package/build/state-style.d.ts +1 -0
- package/build/state-style.js +8 -0
- package/build/style-scope.d.ts +2 -1
- package/build/style-scope.js +10 -8
- package/metro-svelte-transformer.cjs +48 -14
- package/package.json +28 -10
- package/build/modules/animated/AnimatedImage.svelte +0 -115
- package/build/modules/animated/AnimatedImage.svelte.d.ts +0 -5
- package/build/modules/animated/AnimatedScrollView.svelte +0 -92
- package/build/modules/animated/AnimatedScrollView.svelte.d.ts +0 -17
- package/build/modules/animated/AnimatedText.svelte +0 -58
- package/build/modules/animated/AnimatedText.svelte.d.ts +0 -5
- package/build/modules/animated/AnimatedView.svelte +0 -95
- package/build/modules/animated/AnimatedView.svelte.d.ts +0 -5
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
|
-
// TouchableHighlight: built on Pressable.
|
|
3
|
-
// TouchableHighlight.js)
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
2
|
+
// TouchableHighlight: built on Pressable. Ported against RN's own source
|
|
3
|
+
// (.vendors/react-native/.../Components/Touchable/TouchableHighlight.js) after the 2026-08-19
|
|
4
|
+
// audit; Solid migrated first, then React, Vue and Angular, and this is Svelte.
|
|
5
|
+
//
|
|
6
|
+
// RN drives the underlay from THREE Pressability callbacks, not from a `pressed` flag, and the
|
|
7
|
+
// difference is visible: onPress re-shows the underlay and holds it for delayPressOut, so a tap
|
|
8
|
+
// too fast to see still flashes. The previous port derived the highlight from Pressable's
|
|
9
|
+
// `pressed` through `highlightPressedStyle`, which cannot express that hold — the flag is
|
|
10
|
+
// already false by then. The machine itself
|
|
11
|
+
// (createHighlightUnderlayHandlers) is shared; Svelte owns the reactive `shown` cell and the
|
|
12
|
+
// real timers.
|
|
7
13
|
import type { ITouchableHighlightProps } from './touchable-highlight-props';
|
|
8
14
|
|
|
9
15
|
export type { ITouchableHighlightProps };
|
|
@@ -11,28 +17,122 @@
|
|
|
11
17
|
|
|
12
18
|
<script lang="ts">
|
|
13
19
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
createHighlightUnderlayHandlers,
|
|
21
|
+
createHighlightUnderlayRuntime,
|
|
22
|
+
hasTouchablePressHandler,
|
|
23
|
+
resolveHighlightExtraStyles,
|
|
18
24
|
} from '@symbiote-native/components';
|
|
19
|
-
import type {
|
|
25
|
+
import type {
|
|
26
|
+
ISymbioteEvent,
|
|
27
|
+
IStyleProp,
|
|
28
|
+
IViewStyle,
|
|
29
|
+
} from '@symbiote-native/engine';
|
|
20
30
|
import Pressable from '../pressable/index.svelte';
|
|
21
31
|
|
|
32
|
+
function scheduleTimeout(callback: () => void, ms: number): () => void {
|
|
33
|
+
const id = setTimeout(callback, ms);
|
|
34
|
+
return () => clearTimeout(id);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// onPress/onPressIn/onPressOut are INTERCEPTED, not forwarded: RN drives the underlay from
|
|
38
|
+
// those three and then calls the caller's. delayPressOut is CONSUMED (it is the underlay hold),
|
|
39
|
+
// so it must not also reach the host as an unknown Fabric prop. onLongPress deliberately stays
|
|
40
|
+
// in `rest` — Pressable fires it directly — and is only READ here, for the has-handler gate.
|
|
22
41
|
let {
|
|
23
|
-
activeOpacity
|
|
24
|
-
underlayColor
|
|
42
|
+
activeOpacity,
|
|
43
|
+
underlayColor,
|
|
25
44
|
style,
|
|
45
|
+
delayPressOut = 0,
|
|
46
|
+
onPress,
|
|
47
|
+
onPressIn,
|
|
48
|
+
onPressOut,
|
|
49
|
+
onShowUnderlay,
|
|
50
|
+
onHideUnderlay,
|
|
26
51
|
children: content,
|
|
27
52
|
...rest
|
|
28
53
|
}: ITouchableHighlightProps = $props();
|
|
29
54
|
|
|
30
|
-
|
|
31
|
-
|
|
55
|
+
// `shown` is NOT `pressed`: RN holds the underlay past the tap for delayPressOut.
|
|
56
|
+
let shown = $state(false);
|
|
57
|
+
// Setup-scope only, mutated by the machine on every event and never read reactively — same as
|
|
58
|
+
// Pressable's own runtime, never `$state`.
|
|
59
|
+
const runtime = createHighlightUnderlayRuntime();
|
|
60
|
+
|
|
61
|
+
// RN's _hasPressHandler gate: a TouchableHighlight with no press callback is decorative, and
|
|
62
|
+
// flashing an underlay under a touch passing through it is wrong.
|
|
63
|
+
const hasPressHandler = $derived(
|
|
64
|
+
hasTouchablePressHandler({
|
|
65
|
+
onPress,
|
|
66
|
+
onPressIn,
|
|
67
|
+
onPressOut,
|
|
68
|
+
onLongPress: rest.onLongPress,
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Rebuilt whenever the config changes; the runtime persists across rebuilds so an in-flight
|
|
73
|
+
// hide timer survives one.
|
|
74
|
+
const underlay = $derived(
|
|
75
|
+
createHighlightUnderlayHandlers(
|
|
76
|
+
{ delayPressOut, hasPressHandler, schedule: scheduleTimeout },
|
|
77
|
+
runtime,
|
|
78
|
+
{
|
|
79
|
+
setShown(next: boolean): void {
|
|
80
|
+
shown = next;
|
|
81
|
+
},
|
|
82
|
+
onShowUnderlay: () => onShowUnderlay?.(),
|
|
83
|
+
onHideUnderlay: () => onHideUnderlay?.(),
|
|
84
|
+
},
|
|
85
|
+
),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const extra = $derived(
|
|
89
|
+
resolveHighlightExtraStyles({
|
|
90
|
+
shown,
|
|
91
|
+
hasPressHandler,
|
|
92
|
+
underlayColor,
|
|
93
|
+
activeOpacity,
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// ITEM 7 IS DELIBERATELY NOT FIXED HERE, and this is the whole of the divergence from RN. RN
|
|
98
|
+
// clones its single child with `extra.child`, so the lowered opacity lands on the CHILD while
|
|
99
|
+
// the container keeps an opaque underlay; React does that with cloneElement and Vue with
|
|
100
|
+
// cloneVNode. Svelte hands a component an opaque `Snippet` — a function that renders into an
|
|
101
|
+
// anchor. It cannot be introspected, cloned, or given a prop, and it may have several roots, so
|
|
102
|
+
// there is NO way to reach the child short of a permanent wrapper View. That wrapper would sit
|
|
103
|
+
// in the flex chain between this container and the children and silently re-parent any `flex`
|
|
104
|
+
// they declare; the fake Fabric runs no Yoga, so no headless test can measure the damage.
|
|
105
|
+
// Shipping an unmeasurable layout change is worse than the visual approximation, so both styles
|
|
106
|
+
// stay on the container (the child style folded in LAST), exactly as Solid and Angular decided.
|
|
107
|
+
// resolveHighlightExtraStyles keeps them separate regardless, so a future fix — which needs a
|
|
108
|
+
// real device, not a test — is unblocked.
|
|
109
|
+
const containerStyle = $derived<IStyleProp<IViewStyle>>(
|
|
110
|
+
extra === undefined ? style : [style, extra.underlay, extra.child],
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// Visual first, then the caller's callback — RN's order in _createPressabilityConfig.
|
|
114
|
+
function handlePressIn(event: ISymbioteEvent): void {
|
|
115
|
+
underlay.handlePressIn(event);
|
|
116
|
+
onPressIn?.(event);
|
|
117
|
+
}
|
|
118
|
+
function handlePress(event: ISymbioteEvent): void {
|
|
119
|
+
underlay.handlePress(event);
|
|
120
|
+
onPress?.(event);
|
|
121
|
+
}
|
|
122
|
+
function handlePressOut(event: ISymbioteEvent): void {
|
|
123
|
+
underlay.handlePressOut(event);
|
|
124
|
+
onPressOut?.(event);
|
|
32
125
|
}
|
|
33
126
|
</script>
|
|
34
127
|
|
|
35
|
-
<Pressable
|
|
128
|
+
<Pressable
|
|
129
|
+
__minPressDuration={0}
|
|
130
|
+
{...rest}
|
|
131
|
+
style={containerStyle}
|
|
132
|
+
onPress={handlePress}
|
|
133
|
+
onPressIn={handlePressIn}
|
|
134
|
+
onPressOut={handlePressOut}
|
|
135
|
+
>
|
|
36
136
|
{#snippet children()}
|
|
37
137
|
{@render content?.()}
|
|
38
138
|
{/snippet}
|
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
</script>
|
|
14
14
|
|
|
15
15
|
<script lang="ts">
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
backgroundProps,
|
|
18
|
+
selectableBackground,
|
|
19
|
+
} from '@symbiote-native/components';
|
|
17
20
|
import { dlog } from '@symbiote-native/engine';
|
|
18
21
|
import Pressable from '../pressable/index.svelte';
|
|
19
22
|
|
|
@@ -29,13 +32,15 @@
|
|
|
29
32
|
const resolved = $derived(background ?? selectableBackground());
|
|
30
33
|
|
|
31
34
|
$effect(() => {
|
|
32
|
-
dlog(
|
|
35
|
+
dlog(
|
|
36
|
+
`TouchableNativeFeedback render ${resolved.type} useForeground ${useForeground}`,
|
|
37
|
+
);
|
|
33
38
|
});
|
|
34
39
|
|
|
35
40
|
const nativeProps = $derived(backgroundProps(resolved, useForeground));
|
|
36
41
|
</script>
|
|
37
42
|
|
|
38
|
-
<Pressable {...rest}>
|
|
43
|
+
<Pressable __minPressDuration={0} {...rest}>
|
|
39
44
|
{#snippet children()}
|
|
40
45
|
<symbiote-view p={nativeProps}>
|
|
41
46
|
{@render content?.()}
|
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
|
-
// TouchableOpacity: built on Pressable, like the rest of the family.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// (
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// wired into the native Animated node graph — `useNativeDriver`-style native-thread driving is
|
|
16
|
-
// NOT available here until a real Animated adapter binding lands.
|
|
2
|
+
// TouchableOpacity: built on Pressable, like the rest of the family. Ported against RN's own
|
|
3
|
+
// source (.vendors/react-native/.../Components/Touchable/TouchableOpacity.js) after the
|
|
4
|
+
// 2026-08-19 audit found ten divergences shared by every adapter; Solid migrated first, then
|
|
5
|
+
// React, Vue and Angular, and this is Svelte.
|
|
6
|
+
//
|
|
7
|
+
// Shared (framework-agnostic) half: the press-scheduling machine and the resting-opacity math
|
|
8
|
+
// (@symbiote-native/components). Svelte owns only the lifecycle — runes, the real timers, and
|
|
9
|
+
// the Animated wiring.
|
|
10
|
+
//
|
|
11
|
+
// The interim `tweenOpacity` stand-in this file used to carry is GONE. Its header claimed no
|
|
12
|
+
// Animated binding existed for this adapter; that stopped being true when
|
|
13
|
+
// modules/animated landed, and a setTimeout tween can never be native-driven — which is
|
|
14
|
+
// exactly what RN asks for here (useNativeDriver: true, TouchableOpacity.js:242).
|
|
17
15
|
import type { ITouchableOpacityProps } from './touchable-opacity-props';
|
|
16
|
+
import View from '../View.svelte';
|
|
17
|
+
import { createAnimatedComponent } from '../../modules/animated/create-animated-component';
|
|
18
18
|
|
|
19
19
|
export type { ITouchableOpacityProps };
|
|
20
|
+
|
|
21
|
+
// Wrapped here, not imported from modules/animated: that barrel pulls in six `.svelte`
|
|
22
|
+
// components (View/Text/Image/ScrollView/FlatList/SectionList), and this package's vitest has
|
|
23
|
+
// no `.svelte` loader — a smoke test would have to pre-compile the whole tree to reach one
|
|
24
|
+
// Animated.View. Same reasoning, different cause, as sticky-header.svelte's own local wrap.
|
|
25
|
+
// Module scope, so every TouchableOpacity instance shares one component identity.
|
|
26
|
+
const AnimatedView = createAnimatedComponent(View);
|
|
20
27
|
</script>
|
|
21
28
|
|
|
22
29
|
<script lang="ts">
|
|
@@ -24,37 +31,28 @@
|
|
|
24
31
|
createTouchableFeedbackHandlers,
|
|
25
32
|
createTouchableFeedbackRuntime,
|
|
26
33
|
DEFAULT_ACTIVE_OPACITY,
|
|
27
|
-
|
|
28
|
-
OPACITY_ACTIVE_DURATION_MS,
|
|
34
|
+
OPACITY_ACTIVE_GRANT_DURATION_MS,
|
|
29
35
|
OPACITY_INACTIVE_DURATION_MS,
|
|
30
|
-
|
|
36
|
+
TOUCHABLE_MIN_PRESS_DURATION_MS,
|
|
37
|
+
restingOpacityFromStyle,
|
|
31
38
|
} from '@symbiote-native/components';
|
|
32
|
-
import
|
|
39
|
+
import {
|
|
40
|
+
AnimatedMock,
|
|
41
|
+
AnimatedValue,
|
|
42
|
+
Easing,
|
|
43
|
+
Platform,
|
|
44
|
+
dlog,
|
|
45
|
+
timing,
|
|
46
|
+
type ISymbioteEvent,
|
|
47
|
+
} from '@symbiote-native/engine';
|
|
33
48
|
import Pressable from '../pressable/index.svelte';
|
|
34
49
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
durationMs: number,
|
|
42
|
-
onFrame: (value: number) => void,
|
|
43
|
-
): () => void {
|
|
44
|
-
const start = Date.now();
|
|
45
|
-
let cancelled = false;
|
|
46
|
-
function step(): void {
|
|
47
|
-
if (cancelled) return;
|
|
48
|
-
const t = Math.min(1, (Date.now() - start) / durationMs);
|
|
49
|
-
const eased = t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2;
|
|
50
|
-
onFrame(from + (to - from) * eased);
|
|
51
|
-
if (t < 1) setTimeout(step, TWEEN_FRAME_MS);
|
|
52
|
-
}
|
|
53
|
-
step();
|
|
54
|
-
return () => {
|
|
55
|
-
cancelled = true;
|
|
56
|
-
};
|
|
57
|
-
}
|
|
50
|
+
// modules/animated's barrel swaps the WHOLE driver namespace for the mock when the host
|
|
51
|
+
// reports reduced motion; reaching it here would close the import the module block above
|
|
52
|
+
// avoids, so the one driver this component uses is swapped locally on the same flag.
|
|
53
|
+
const startTiming = Platform.isDisableAnimations
|
|
54
|
+
? AnimatedMock.timing
|
|
55
|
+
: timing;
|
|
58
56
|
|
|
59
57
|
function scheduleTimeout(callback: () => void, ms: number): () => void {
|
|
60
58
|
const id = setTimeout(callback, ms);
|
|
@@ -70,21 +68,36 @@
|
|
|
70
68
|
onPressOut,
|
|
71
69
|
delayPressIn = 0,
|
|
72
70
|
delayPressOut = 0,
|
|
73
|
-
|
|
71
|
+
// RN's Touchables override Pressability's own 130ms floor with 0 (TouchableOpacity.js:195);
|
|
72
|
+
// what holds the active visual is the fade's duration, not a press-duration floor.
|
|
73
|
+
minPressDuration = TOUCHABLE_MIN_PRESS_DURATION_MS,
|
|
74
74
|
...rest
|
|
75
75
|
}: ITouchableOpacityProps = $props();
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
// RN's _getChildStyleOpacityWithDefault: the fade settles at the opacity the CALLER's style
|
|
78
|
+
// asks for, not at a hard 1.
|
|
79
|
+
const restingOpacity = $derived(restingOpacityFromStyle(style));
|
|
80
|
+
|
|
81
|
+
// One Animated.Value per mount, held by IDENTITY in a plain `const` — never `$state`, which
|
|
82
|
+
// would deep-proxy an engine object whose graph bookkeeping is keyed on the raw instance
|
|
83
|
+
// (same concern as Pressable's `$state.raw` hostShim).
|
|
84
|
+
// svelte-ignore state_referenced_locally -- the SEED is a one-shot read of the initial style;
|
|
85
|
+
// later changes are re-settled by the update effect below, not by re-seeding.
|
|
86
|
+
const opacity = new AnimatedValue(restingOpacity);
|
|
87
|
+
|
|
79
88
|
// Setup-scope only, mutated by the machine — same as Pressable's own runtime, never `$state`.
|
|
80
89
|
const runtime = createTouchableFeedbackRuntime();
|
|
81
90
|
|
|
82
91
|
function setOpacityTo(toValue: number, duration: number): void {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
92
|
+
dlog(`TouchableOpacity opacity -> ${toValue} over ${duration}ms`);
|
|
93
|
+
// useNativeDriver: true is RN's own (TouchableOpacity.js:242). opacity is natively drivable,
|
|
94
|
+
// so the fade survives a busy JS thread — the whole point of press feedback.
|
|
95
|
+
startTiming(opacity, {
|
|
96
|
+
toValue,
|
|
97
|
+
duration,
|
|
98
|
+
easing: Easing.inOut(Easing.quad),
|
|
99
|
+
useNativeDriver: true,
|
|
100
|
+
}).start();
|
|
88
101
|
}
|
|
89
102
|
|
|
90
103
|
// Rebuilt whenever the timing config changes (mirrors Vue's per-render rebuild); the runtime
|
|
@@ -101,24 +114,68 @@
|
|
|
101
114
|
runtime,
|
|
102
115
|
{
|
|
103
116
|
activate(event: ISymbioteEvent): void {
|
|
104
|
-
|
|
117
|
+
// 0, not 150 (TouchableOpacity.js:215-220): the duration is chosen by where the
|
|
118
|
+
// press-in came from, and Pressability re-dispatches the GRANT event as the delay
|
|
119
|
+
// signal when delayPressIn is 0 — so an ordinary tap darkens instantly in RN. The 150ms
|
|
120
|
+
// branch is a re-activation (finger returning inside after leaving the bounds), which
|
|
121
|
+
// our engine never emits — it sends pressIn from one place, on topTouchStart.
|
|
122
|
+
setOpacityTo(activeOpacity, OPACITY_ACTIVE_GRANT_DURATION_MS);
|
|
105
123
|
onPressIn?.(event);
|
|
106
124
|
},
|
|
107
125
|
deactivate(event: ISymbioteEvent): void {
|
|
108
|
-
setOpacityTo(
|
|
126
|
+
setOpacityTo(restingOpacity, OPACITY_INACTIVE_DURATION_MS);
|
|
109
127
|
onPressOut?.(event);
|
|
110
128
|
},
|
|
111
129
|
},
|
|
112
130
|
),
|
|
113
131
|
);
|
|
114
132
|
|
|
115
|
-
|
|
133
|
+
// Two SEPARATE deriveds, not one object: each memoizes by value, so an unrelated prop change
|
|
134
|
+
// cannot re-settle the opacity — the Svelte form of Vue's deliberate two-getter watch source.
|
|
135
|
+
const disabledInput = $derived(rest.disabled);
|
|
136
|
+
|
|
137
|
+
// RN's componentDidUpdate: a changed `disabled` or a changed style opacity re-settles the view,
|
|
138
|
+
// so a Touchable disabled mid-press does not stay stuck at its active opacity. The first run is
|
|
139
|
+
// skipped — RN does this on UPDATE only, and firing at mount would animate over the value the
|
|
140
|
+
// Animated.Value was just seeded with.
|
|
141
|
+
let settled: { disabled: unknown; resting: number } | undefined;
|
|
142
|
+
$effect(() => {
|
|
143
|
+
const current = { disabled: disabledInput, resting: restingOpacity };
|
|
144
|
+
const previous = settled;
|
|
145
|
+
settled = current;
|
|
146
|
+
if (previous === undefined) return;
|
|
147
|
+
if (
|
|
148
|
+
previous.disabled === current.disabled &&
|
|
149
|
+
previous.resting === current.resting
|
|
150
|
+
) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
setOpacityTo(current.resting, OPACITY_INACTIVE_DURATION_MS);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// RN's componentWillUnmount: stop the animation and drop the value back, so a teardown
|
|
157
|
+
// mid-fade leaves no driver running against a node that is gone.
|
|
158
|
+
$effect(() => {
|
|
159
|
+
return () => {
|
|
160
|
+
opacity.resetAnimation();
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Depends on `style` alone. `opacity` is a stable graph node, so a press changes what the node
|
|
165
|
+
// HOLDS, never this array — the per-frame path is setValue -> AnimatedProps.update ->
|
|
166
|
+
// setNativeProps, which never re-renders this component.
|
|
167
|
+
const feedbackStyle = $derived([style, { opacity }]);
|
|
116
168
|
</script>
|
|
117
169
|
|
|
118
|
-
<Pressable
|
|
170
|
+
<Pressable
|
|
171
|
+
__minPressDuration={0}
|
|
172
|
+
{...rest}
|
|
173
|
+
onPressIn={handlers.handlePressIn}
|
|
174
|
+
onPressOut={handlers.handlePressOut}
|
|
175
|
+
>
|
|
119
176
|
{#snippet children()}
|
|
120
|
-
<
|
|
177
|
+
<AnimatedView style={feedbackStyle} class={className}>
|
|
121
178
|
{@render content?.()}
|
|
122
|
-
</
|
|
179
|
+
</AnimatedView>
|
|
123
180
|
{/snippet}
|
|
124
181
|
</Pressable>
|
|
@@ -1,18 +1,74 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
|
-
// TouchableWithoutFeedback: built on Pressable, no visual feedback of its own
|
|
3
|
-
//
|
|
2
|
+
// TouchableWithoutFeedback: built on Pressable, no visual feedback of its own. Ported against
|
|
3
|
+
// RN's own source (.vendors/react-native/.../Components/Touchable/TouchableWithoutFeedback.js)
|
|
4
|
+
// after the 2026-08-19 audit.
|
|
5
|
+
//
|
|
6
|
+
// RN builds a FULL Pressability config here — delayPressIn / delayPressOut /
|
|
7
|
+
// minPressDuration: 0 — so "without feedback" means no VISUAL, not no TIMING. The previous port
|
|
8
|
+
// spread the delay props straight into Pressable, which both ignored them (Pressable owns no
|
|
9
|
+
// press-delay scheduling) and forwarded them to the host as unknown Fabric props. The same
|
|
10
|
+
// shared machine TouchableOpacity runs now runs here with the visual half left empty.
|
|
4
11
|
import type { ITouchableWithoutFeedbackProps } from './touchable-without-feedback-props';
|
|
5
12
|
|
|
6
13
|
export type { ITouchableWithoutFeedbackProps };
|
|
7
14
|
</script>
|
|
8
15
|
|
|
9
16
|
<script lang="ts">
|
|
17
|
+
import {
|
|
18
|
+
createTouchableFeedbackHandlers,
|
|
19
|
+
createTouchableFeedbackRuntime,
|
|
20
|
+
TOUCHABLE_MIN_PRESS_DURATION_MS,
|
|
21
|
+
} from '@symbiote-native/components';
|
|
22
|
+
import type { ISymbioteEvent } from '@symbiote-native/engine';
|
|
10
23
|
import Pressable from '../pressable/index.svelte';
|
|
11
24
|
|
|
12
|
-
|
|
25
|
+
function scheduleTimeout(callback: () => void, ms: number): () => void {
|
|
26
|
+
const id = setTimeout(callback, ms);
|
|
27
|
+
return () => clearTimeout(id);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let {
|
|
31
|
+
children: content,
|
|
32
|
+
onPressIn,
|
|
33
|
+
onPressOut,
|
|
34
|
+
delayPressIn = 0,
|
|
35
|
+
delayPressOut = 0,
|
|
36
|
+
// RN's Touchables override Pressability's own 130ms floor with 0.
|
|
37
|
+
minPressDuration = TOUCHABLE_MIN_PRESS_DURATION_MS,
|
|
38
|
+
...rest
|
|
39
|
+
}: ITouchableWithoutFeedbackProps = $props();
|
|
40
|
+
|
|
41
|
+
// Setup-scope only, mutated by the machine — never `$state`.
|
|
42
|
+
const runtime = createTouchableFeedbackRuntime();
|
|
43
|
+
|
|
44
|
+
const handlers = $derived(
|
|
45
|
+
createTouchableFeedbackHandlers(
|
|
46
|
+
{
|
|
47
|
+
delayPressIn,
|
|
48
|
+
delayPressOut,
|
|
49
|
+
minPressDuration,
|
|
50
|
+
schedule: scheduleTimeout,
|
|
51
|
+
now: Date.now,
|
|
52
|
+
},
|
|
53
|
+
runtime,
|
|
54
|
+
{
|
|
55
|
+
activate(event: ISymbioteEvent): void {
|
|
56
|
+
onPressIn?.(event);
|
|
57
|
+
},
|
|
58
|
+
deactivate(event: ISymbioteEvent): void {
|
|
59
|
+
onPressOut?.(event);
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
),
|
|
63
|
+
);
|
|
13
64
|
</script>
|
|
14
65
|
|
|
15
|
-
<Pressable
|
|
66
|
+
<Pressable
|
|
67
|
+
__minPressDuration={0}
|
|
68
|
+
{...rest}
|
|
69
|
+
onPressIn={handlers.handlePressIn}
|
|
70
|
+
onPressOut={handlers.handlePressOut}
|
|
71
|
+
>
|
|
16
72
|
{#snippet children()}
|
|
17
73
|
{@render content?.()}
|
|
18
74
|
{/snippet}
|