@gryt/ui-native 0.6.0 → 0.6.1

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.
@@ -1 +1 @@
1
- {"version":3,"file":"Drawer.d.ts","sourceRoot":"","sources":["../../../src/components/Drawer/Drawer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,SAAS,EAA2C,MAAM,OAAO,CAAC;AAC/F,OAAO,EAKL,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAWtB,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAG/E,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAgBrD,MAAM,WAAW,eAAgB,SAAQ,cAAc;IACrD,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,iBAAS,IAAI,CAAC,EAAE,QAAQ,EAAE,GAAG,SAAS,EAAE,EAAE,eAAe,+BAKxD;AAED,iBAAS,OAAO,CAAC,EACf,QAAQ,EACR,KAAK,EACN,EAAE;IACD,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B,+BAWA;AAED,iBAAS,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,CAAC,EAAE,SAAS,CAAA;CAAE,+BAErD;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,sCAAsC;IACtC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAgBD,iBAAS,KAAK,CAAC,EACb,QAAQ,EACR,IAAa,EACb,IAAU,EACV,WAAkB,EAClB,KAAK,EACN,EAAE,gBAAgB,+BAiSlB;AAED,iBAAS,KAAK,CAAC,EACb,QAAQ,EACR,KAAK,EACN,EAAE;IACD,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B,+BAWA;AAED,eAAO,MAAM,MAAM;;;;;;CAA0C,CAAC"}
1
+ {"version":3,"file":"Drawer.d.ts","sourceRoot":"","sources":["../../../src/components/Drawer/Drawer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,SAAS,EAKf,MAAM,OAAO,CAAC;AACf,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAYtB,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAG/E,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAgBrD,MAAM,WAAW,eAAgB,SAAQ,cAAc;IACrD,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,iBAAS,IAAI,CAAC,EAAE,QAAQ,EAAE,GAAG,SAAS,EAAE,EAAE,eAAe,+BAKxD;AAED,iBAAS,OAAO,CAAC,EACf,QAAQ,EACR,KAAK,EACN,EAAE;IACD,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B,+BAWA;AAED,iBAAS,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,CAAC,EAAE,SAAS,CAAA;CAAE,+BAErD;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,sCAAsC;IACtC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAgBD,iBAAS,KAAK,CAAC,EACb,QAAQ,EACR,IAAa,EACb,IAAU,EACV,WAAkB,EAClB,KAAK,EACN,EAAE,gBAAgB,+BA0UlB;AAED,iBAAS,KAAK,CAAC,EACb,QAAQ,EACR,KAAK,EACN,EAAE;IACD,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B,+BAWA;AAED,eAAO,MAAM,MAAM;;;;;;CAA0C,CAAC"}
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { createContext, useContext, useEffect, useRef, useState } from "react";
3
- import { Dimensions, PanResponder, Modal, Pressable } from "react-native";
2
+ import { createContext, useContext, useEffect, useMemo, useState } from "react";
3
+ import { Dimensions, Modal, Pressable } from "react-native";
4
+ import { Gesture, GestureDetector } from "react-native-gesture-handler";
4
5
  import Animated, { runOnJS, useAnimatedStyle, useSharedValue } from "react-native-reanimated";
5
6
  import { useSafeAreaInsets } from "react-native-safe-area-context";
6
7
  import { useReducedMotion } from "../../hooks/useReducedMotion.js";
@@ -79,7 +80,11 @@ function Popup({ children, side = "left", size = 0.8, dismissible = true, style
79
80
  const panelExtent = extent + bleed;
80
81
  // Measured against the bleed panel, so the panel's *visible* edge lands
81
82
  // exactly off-screen rather than the overhang doing it.
82
- const hidden = side === "right" ? panelExtent : side === "left" ? -panelExtent : panelExtent;
83
+ const hidden = side === "right"
84
+ ? panelExtent
85
+ : side === "left"
86
+ ? -panelExtent
87
+ : panelExtent;
83
88
  /**
84
89
  * 700ms on the *tight* curve, and this changed on both platforms at once.
85
90
  *
@@ -150,141 +155,172 @@ function Popup({ children, side = "left", size = 0.8, dismissible = true, style
150
155
  };
151
156
  });
152
157
  /**
153
- * The scrim thins as the panel is dragged away, and does not fade in or out.
158
+ * The scrim fades with the panel, and thins further as it is dragged away.
154
159
  *
155
- * A drawer opens by sliding and nothing about it fades — that is the whole
156
- * distinction from a dialog, and a scrim that fades up alongside the panel
157
- * puts a second, slower animation on top of the one that matters. It is up
158
- * when the drawer is up.
160
+ * Two separate rules, both the web's:
159
161
  *
160
- * The drag term stays, and it is a different thing: pushing the panel off
161
- * lightens the scrim in proportion, so a half-gone drawer does not sit under
162
- * a full-strength one. The web's own rule, in its own words.
162
+ * ```
163
+ * "[opacity:calc(1-var(--drawer-swipe-progress,0))]",
164
+ * "transition-opacity duration-(--gryt-dur-spring-soft) ease-spring-tight",
165
+ * "data-starting-style:opacity-0 data-ending-style:opacity-0",
166
+ * ```
163
167
  *
164
- * The Modal unmounts once the panel has finished travelling, which is what
165
- * takes the scrim with it. Earlier this was a flat colour that vanished with
166
- * the Modal *before* the panel had moved, so a drawer animating out sat under
167
- * a full-strength scrim for 700ms and then blinked off that was the bug,
168
- * and keeping the scrim up for exactly as long as the panel is on screen is
169
- * the fix rather than fading it.
168
+ * The first is the drag term. The other two are the fade: the backdrop
169
+ * starts and ends at zero, over the same duration and easing as the panel's
170
+ * slide. `progress` already runs on `travel`, which is `easeSpringTight` at
171
+ * `springSoft` so multiplying by it is the same curve, not a new one.
172
+ *
173
+ * GRYT-408 took the fade out, on the grounds that "the web's Popup declares
174
+ * `transition-transform` and nothing else". That is true, and it is about
175
+ * `Drawer.Popup` — the panel, which still only translates. The backdrop is a
176
+ * different element and says the opposite. Without the fade the scrim was at
177
+ * full strength before the panel was on screen and stayed there while it slid
178
+ * away, then blinked off with the Modal.
170
179
  */
171
180
  const scrimStyle = useAnimatedStyle(() => {
172
181
  const dragged = extent > 0 ? Math.min(1, Math.abs(drag.value) / extent) : 0;
173
- return { opacity: 1 - dragged };
182
+ return { opacity: progress.value * (1 - dragged) };
174
183
  });
175
184
  /**
176
185
  * Swipe to dismiss, which the web has had from Base UI's drawer primitive
177
186
  * and this did not have at all (GRYT-395). A drawer you cannot push back is
178
187
  * the thing a drawer is for.
179
188
  *
180
- * `PanResponder` rather than react-native-gesture-handler: the Slider proved
181
- * this path works from this package's prebuilt output today, and an attempt
182
- * at gesture-handler from here failed for reasons still not understood
183
- * (GRYT-393). This is not the place to retry that.
189
+ * `react-native-gesture-handler` rather than `PanResponder`. That was the
190
+ * other way round because gesture callbacks from this package's prebuilt
191
+ * output were measured doing nothing (GRYT-393) the babel plugin was not
192
+ * reaching `node_modules`, so nothing became a worklet. Measured again on
193
+ * 2026-08-21 with a probe built inside this package: both worklet and
194
+ * `runOnJS` callbacks report correctly. Whatever it was, it is fixed, and
195
+ * `PanResponder` was only ever the fallback.
196
+ *
197
+ * The reason to move is what the axis constraints below buy. `PanResponder`
198
+ * cannot negotiate with a native scroll recogniser, so the old version kept
199
+ * the drag with `onPanResponderTerminationRequest: () => false` — which
200
+ * works by never handing the gesture back, and stops a `ScrollView` inside
201
+ * the drawer scrolling at all. Here the pan simply fails on a cross-axis
202
+ * drag and the scrollable gets it, which is the same rule the web relies on
203
+ * the browser for.
204
+ *
205
+ * The callbacks stay worklets. Running them on the JS thread would be
206
+ * simpler and would give up the whole point: the panel tracks the finger on
207
+ * the UI thread, with nothing to be blocked by a busy bridge.
184
208
  */
185
- const gesture = useRef({ extent, side, vertical, dismissible, setOpen });
186
- useEffect(() => {
187
- gesture.current = { extent, side, vertical, dismissible, setOpen };
188
- }, [extent, side, vertical, dismissible, setOpen]);
189
- /* eslint-disable react-hooks/refs */
190
- const [responder] = useState(() => PanResponder.create({
191
- // Claimed on move, not on start. Claiming on start would swallow every
192
- // tap on the panel's own content buttons inside a drawer would stop
193
- // working, which is a far worse bug than not being able to swipe.
194
- onStartShouldSetPanResponder: () => false,
195
- onMoveShouldSetPanResponder: (_e, g) => {
196
- if (!gesture.current.dismissible)
197
- return false;
198
- // Only once the drag is clearly along the axis the panel closes on,
199
- // and clearly a drag rather than a slow press. Anything else belongs
200
- // to whatever is inside.
201
- const along = gesture.current.vertical ? g.dy : g.dx;
202
- const across = gesture.current.vertical ? g.dx : g.dy;
203
- return Math.abs(along) > 8 && Math.abs(along) > Math.abs(across);
204
- },
205
- // Once it is ours it stays ours, for the same reason the Slider needs
206
- // this: the default is to grant, and a ScrollView inside the drawer
207
- // would take the drag back mid-swipe.
208
- onPanResponderTerminationRequest: () => false,
209
- onPanResponderMove: (_e, g) => {
210
- const { vertical: v, side: sd } = gesture.current;
211
- const raw = v ? g.dy : g.dx;
209
+ const pan = useMemo(() => {
210
+ const closingSign = side === "left" ? -1 : 1;
211
+ const gesture = Gesture.Pan().enabled(dismissible);
212
+ /* Claim only a drag heading the way the panel closes, and only once it is
213
+ * clearly a drag rather than a slow press — 8pt, the same threshold the
214
+ * old responder used. `failOffset` on the other axis is the half that
215
+ * `PanResponder` had no answer for: a finger moving across the panel is
216
+ * somebody scrolling its contents, and this hands that over rather than
217
+ * swallowing it. */
218
+ if (vertical) {
219
+ gesture.activeOffsetY(8).failOffsetX([-12, 12]);
220
+ }
221
+ else {
222
+ gesture.activeOffsetX(closingSign * 8).failOffsetY([-12, 12]);
223
+ }
224
+ return gesture
225
+ .onUpdate((event) => {
226
+ "worklet";
227
+ const raw = vertical ? event.translationY : event.translationX;
212
228
  // Clamped to the closing direction. Dragging a left drawer rightwards
213
229
  // should do nothing, not tear it off its edge.
214
- const closing = sd === "left" ? Math.min(0, raw) : Math.max(0, raw);
215
- // eslint-disable-next-line react-hooks/immutability
216
- drag.value = closing;
217
- },
218
- onPanResponderRelease: (_e, g) => {
219
- const { vertical: v, side: sd, extent: ex } = gesture.current;
220
- const moved = Math.abs(v ? g.dy : g.dx);
221
- const speed = Math.abs(v ? g.vy : g.vx);
222
- // Half the panel, or a flick. The velocity term is what makes a short
223
- // sharp swipe work — without it you have to drag the whole way, which
224
- // is the difference between a drawer and a slow puzzle.
225
- const dismiss = moved > ex / 2 || speed > 0.5;
226
- if (dismiss) {
227
- gesture.current.setOpen(false);
228
- // Left where it is: the close animation runs from here, and resetting
229
- // it first would snap the panel back before it left.
230
+ drag.value = closingSign < 0 ? Math.min(0, raw) : Math.max(0, raw);
231
+ })
232
+ .onEnd((event) => {
233
+ "worklet";
234
+ const moved = Math.abs(vertical ? event.translationY : event.translationX);
235
+ const speed = Math.abs(vertical ? event.velocityY : event.velocityX);
236
+ /* Half the panel, or a flick. The velocity term is what makes a short
237
+ * sharp swipe work without it you have to drag the whole way, which
238
+ * is the difference between a drawer and a slow puzzle.
239
+ *
240
+ * Gesture-handler reports velocity in points per second where
241
+ * `PanResponder` reported points per millisecond, so the old `0.5`
242
+ * became 500. Same gesture, different unit — worth stating, because
243
+ * carrying the number across unchanged would have made a flick need to
244
+ * be a thousand times faster and looked like the velocity term simply
245
+ * not working.
246
+ */
247
+ if (moved > extent / 2 || speed > 500) {
248
+ // Left where it is: the close animation runs from here, and snapping
249
+ // it back first would show the panel returning before it left.
250
+ runOnJS(setOpen)(false);
230
251
  return;
231
252
  }
232
- // eslint-disable-next-line react-hooks/immutability
233
253
  drag.value = travelTo(0, { duration: durations.springSoft });
234
- },
235
- onPanResponderTerminate: () => {
254
+ })
255
+ .onFinalize((_event, success) => {
256
+ "worklet";
257
+ // A gesture cancelled by the system — an incoming call, a parent
258
+ // taking over — leaves the panel wherever the finger was.
259
+ if (!success)
260
+ drag.value = travelTo(0, { duration: durations.springSoft });
261
+ });
262
+ }, [dismissible, drag, extent, setOpen, side, vertical]);
263
+ /**
264
+ * Any leftover drag goes back as the panel opens, and is cleared once it is
265
+ * gone.
266
+ *
267
+ * Two moments, deliberately. Clearing on `open` going false was wrong: that
268
+ * happens the instant a swipe dismisses, so the panel jumped back to fully
269
+ * open and then slid out — exactly what the release handler above says must
270
+ * not happen (GRYT-429). `mounted` is the moment the panel is actually off
271
+ * screen, where a hard reset costs nothing to look at.
272
+ */
273
+ useEffect(() => {
274
+ if (open) {
236
275
  // eslint-disable-next-line react-hooks/immutability
237
276
  drag.value = travelTo(0, { duration: durations.springSoft });
238
277
  }
239
- }));
240
- /* eslint-enable react-hooks/refs */
241
- // Cleared once the panel is closed, so the next open does not start from
242
- // wherever the last swipe left it.
278
+ }, [open, drag]);
243
279
  useEffect(() => {
244
- if (!open) {
280
+ if (!mounted) {
245
281
  // eslint-disable-next-line react-hooks/immutability
246
282
  drag.value = 0;
247
283
  }
248
- }, [open, drag]);
249
- return (_jsx(Modal, { visible: mounted, transparent: true, animationType: "none", onRequestClose: dismissible ? () => setOpen(false) : undefined, children: _jsx(AnimatedPressable, { onPress: dismissible ? () => setOpen(false) : undefined, style: [{ flex: 1, backgroundColor: "rgba(0,0,0,0.6)" }, scrimStyle], children: _jsx(Animated.View, { accessibilityViewIsModal: true, ...responder.panHandlers, style: [
250
- {
251
- position: "absolute",
252
- // The overhang hangs *off* the edge the panel comes from, which
253
- // is the only direction that helps. It used to be added to the
254
- // width instead, so the panel grew inwards — 64pt more of the
255
- // screen covered, and the seam it was meant to hide still there,
256
- // because the gap opens on the entering edge and that is the edge
257
- // the extra material was not on.
258
- top: side === "bottom" ? undefined : 0,
259
- bottom: vertical ? -bleed : 0,
260
- left: side === "right" ? undefined : side === "left" ? -bleed : 0,
261
- right: side === "left" ? undefined : side === "right" ? -bleed : 0,
262
- width: vertical ? "100%" : panelExtent,
263
- height: vertical ? panelExtent : "100%",
264
- // Puts the content back where it would have been without the
265
- // overhang, so `size` still means what it says.
266
- paddingLeft: side === "left" ? bleed : 0,
267
- paddingRight: side === "right" ? bleed : 0,
268
- paddingBottom: vertical ? bleed : 0,
269
- backgroundColor: theme.color.surface,
270
- borderColor: theme.color.border,
271
- borderRightWidth: side === "left" ? 1 : 0,
272
- borderLeftWidth: side === "right" ? 1 : 0,
273
- borderTopWidth: vertical ? 1 : 0,
274
- borderTopLeftRadius: vertical ? theme.radius.lg : 0,
275
- borderTopRightRadius: vertical ? theme.radius.lg : 0
276
- },
277
- panelStyle
278
- ], children: _jsx(Pressable, { onPress: () => { }, style: [
284
+ }, [mounted, drag]);
285
+ return (_jsx(Modal, { visible: mounted, transparent: true, animationType: "none", onRequestClose: dismissible ? () => setOpen(false) : undefined, children: _jsx(AnimatedPressable, { onPress: dismissible ? () => setOpen(false) : undefined, style: [{ flex: 1, backgroundColor: "rgba(0,0,0,0.6)" }, scrimStyle], children: _jsx(GestureDetector, { gesture: pan, children: _jsx(Animated.View, { accessibilityViewIsModal: true, style: [
279
286
  {
280
- flex: 1,
281
- // Before the caller's style, so a drawer that wants to run
282
- // under the island a full-bleed image, say still can.
283
- paddingTop: vertical ? 0 : insets.top,
284
- paddingBottom: insets.bottom
287
+ position: "absolute",
288
+ // The overhang hangs *off* the edge the panel comes from, which
289
+ // is the only direction that helps. It used to be added to the
290
+ // width instead, so the panel grew inwards — 64pt more of the
291
+ // screen covered, and the seam it was meant to hide still there,
292
+ // because the gap opens on the entering edge and that is the edge
293
+ // the extra material was not on.
294
+ top: side === "bottom" ? undefined : 0,
295
+ bottom: vertical ? -bleed : 0,
296
+ left: side === "right" ? undefined : side === "left" ? -bleed : 0,
297
+ right: side === "left" ? undefined : side === "right" ? -bleed : 0,
298
+ width: vertical ? "100%" : panelExtent,
299
+ height: vertical ? panelExtent : "100%",
300
+ // Puts the content back where it would have been without the
301
+ // overhang, so `size` still means what it says.
302
+ paddingLeft: side === "left" ? bleed : 0,
303
+ paddingRight: side === "right" ? bleed : 0,
304
+ paddingBottom: vertical ? bleed : 0,
305
+ backgroundColor: theme.color.surface,
306
+ borderColor: theme.color.border,
307
+ borderRightWidth: side === "left" ? 1 : 0,
308
+ borderLeftWidth: side === "right" ? 1 : 0,
309
+ borderTopWidth: vertical ? 1 : 0,
310
+ borderTopLeftRadius: vertical ? theme.radius.lg : 0,
311
+ borderTopRightRadius: vertical ? theme.radius.lg : 0
285
312
  },
286
- style
287
- ], children: children }) }) }) }));
313
+ panelStyle
314
+ ], children: _jsx(Pressable, { onPress: () => { }, style: [
315
+ {
316
+ flex: 1,
317
+ // Before the caller's style, so a drawer that wants to run
318
+ // under the island — a full-bleed image, say — still can.
319
+ paddingTop: vertical ? 0 : insets.top,
320
+ paddingBottom: insets.bottom
321
+ },
322
+ style
323
+ ], children: children }) }) }) }) }));
288
324
  }
289
325
  function Close({ children, style }) {
290
326
  const { setOpen } = useDrawer("Close");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gryt/ui-native",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Gryt's design system on React Native. Same tokens as @gryt/ui, a different renderer.",