@gryt/ui-native 0.2.0 → 0.3.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.
Files changed (39) hide show
  1. package/dist/components/AlertDialog/AlertDialog.d.ts.map +1 -1
  2. package/dist/components/Button/Button.d.ts.map +1 -1
  3. package/dist/components/Button/Button.js +14 -14
  4. package/dist/components/Collapsible/Collapsible.d.ts +14 -6
  5. package/dist/components/Collapsible/Collapsible.d.ts.map +1 -1
  6. package/dist/components/Collapsible/Collapsible.js +31 -10
  7. package/dist/components/Dialog/Dialog.d.ts.map +1 -1
  8. package/dist/components/Dialog/Dialog.js +16 -3
  9. package/dist/components/Drawer/Drawer.d.ts +1 -1
  10. package/dist/components/Drawer/Drawer.d.ts.map +1 -1
  11. package/dist/components/Drawer/Drawer.js +46 -41
  12. package/dist/components/Progress/Progress.d.ts +17 -6
  13. package/dist/components/Progress/Progress.d.ts.map +1 -1
  14. package/dist/components/Progress/Progress.js +17 -6
  15. package/dist/components/Slider/Slider.d.ts.map +1 -1
  16. package/dist/components/Slider/Slider.js +51 -19
  17. package/dist/components/Slider/sliderValue.d.ts +15 -0
  18. package/dist/components/Slider/sliderValue.d.ts.map +1 -0
  19. package/dist/components/Slider/sliderValue.js +23 -0
  20. package/dist/components/Switch/Switch.d.ts.map +1 -1
  21. package/dist/components/Switch/Switch.js +23 -19
  22. package/dist/components/Toast/Toast.d.ts.map +1 -1
  23. package/dist/components/Toast/Toast.js +12 -8
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +1 -0
  27. package/dist/motion/easing.d.ts +28 -0
  28. package/dist/motion/easing.d.ts.map +1 -0
  29. package/dist/motion/easing.js +51 -0
  30. package/dist/motion/index.d.ts +4 -0
  31. package/dist/motion/index.d.ts.map +1 -0
  32. package/dist/motion/index.js +3 -0
  33. package/dist/motion/motion.d.ts +15 -0
  34. package/dist/motion/motion.d.ts.map +1 -0
  35. package/dist/motion/motion.js +32 -0
  36. package/dist/motion/usePressScale.d.ts +15 -0
  37. package/dist/motion/usePressScale.d.ts.map +1 -0
  38. package/dist/motion/usePressScale.js +39 -0
  39. package/package.json +8 -5
@@ -1 +1 @@
1
- {"version":3,"file":"AlertDialog.d.ts","sourceRoot":"","sources":["../../../src/components/AlertDialog/AlertDialog.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEvF;;;;;;;;;;GAUG;AAIH,MAAM,MAAM,qBAAqB,GAAG,IAAI,CAAC,gBAAgB,EAAE,aAAa,CAAC,CAAC;AAE1E,iBAAS,KAAK,CAAC,KAAK,EAAE,qBAAqB,+BAE1C;AAED,MAAM,MAAM,oBAAoB,GAAG,eAAe,CAAC;AAEnD,eAAO,MAAM,WAAW;;;;gBAUw0B,CAAC;;;;;gBAAy/E,CAAC;aAAkB,CAAC;;;gBAAiS,CAAC;aAAkB,CAAC;;;gBAAkiB,CAAC;aAAkB,CAAC;;CADxtI,CAAC"}
1
+ {"version":3,"file":"AlertDialog.d.ts","sourceRoot":"","sources":["../../../src/components/AlertDialog/AlertDialog.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEvF;;;;;;;;;;GAUG;AAIH,MAAM,MAAM,qBAAqB,GAAG,IAAI,CAAC,gBAAgB,EAAE,aAAa,CAAC,CAAC;AAE1E,iBAAS,KAAK,CAAC,KAAK,EAAE,qBAAqB,+BAE1C;AAED,MAAM,MAAM,oBAAoB,GAAG,eAAe,CAAC;AAEnD,eAAO,MAAM,WAAW;;;;gBAUw0B,CAAC;;;;;gBAAk6H,CAAC;aAAkB,CAAC;;;gBAAiS,CAAC;aAAkB,CAAC;;;gBAAkiB,CAAC;aAAkB,CAAC;;CADjoL,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"Button.d.ts","sourceRoot":"","sources":["../../../src/components/Button/Button.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACjD,OAAO,EAKL,KAAK,cAAc,EACnB,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAKtB,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;AAClF,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AASjE,MAAM,WAAW,WAAY,SAAQ,IAAI,CAAC,cAAc,EAAE,OAAO,GAAG,UAAU,CAAC;IAC7E,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAKD,wBAAgB,MAAM,CAAC,EACrB,QAAQ,EACR,IAAgB,EAChB,IAAe,EACf,SAAS,EACT,OAAO,EACP,KAAK,EACL,QAAgB,EAChB,QAAQ,EACR,GAAG,IAAI,EACR,EAAE,WAAW,+BAgFb"}
1
+ {"version":3,"file":"Button.d.ts","sourceRoot":"","sources":["../../../src/components/Button/Button.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,EAIL,KAAK,cAAc,EACnB,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAQtB,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;AAClF,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AASjE,MAAM,WAAW,WAAY,SAAQ,IAAI,CAAC,cAAc,EAAE,OAAO,GAAG,UAAU,CAAC;IAC7E,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAWD,wBAAgB,MAAM,CAAC,EACrB,QAAQ,EACR,IAAgB,EAChB,IAAe,EACf,SAAS,EACT,OAAO,EACP,KAAK,EACL,QAAgB,EAChB,QAAQ,EACR,GAAG,IAAI,EACR,EAAE,WAAW,+BAuEb"}
@@ -1,7 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from "react";
3
- import { Animated, Pressable, Text, View, } from "react-native";
2
+ import { Pressable, Text, View, } from "react-native";
3
+ import Animated from "react-native-reanimated";
4
+ import { grytScaleSteps } from "@gryt/theme";
4
5
  import { useReducedMotion } from "../../hooks/useReducedMotion.js";
6
+ import { usePressScale } from "../../motion/index.js";
5
7
  import { useTheme } from "../../theme/index.js";
6
8
  const SIZES = {
7
9
  xsmall: { minHeight: 32, paddingH: 12, fontSize: 12 },
@@ -9,17 +11,20 @@ const SIZES = {
9
11
  medium: { minHeight: 40, paddingH: 20, fontSize: 14 },
10
12
  large: { minHeight: 48, paddingH: 24, fontSize: 16 },
11
13
  };
12
- const PRESSED_SCALE = 0.96;
13
- const SPRING = { useNativeDriver: true, speed: 40, bounciness: 6 };
14
+ /**
15
+ * `active:scale-[0.96]` on the web, and the same number here.
16
+ *
17
+ * `hover:scale-[1.03]` has no counterpart: a touch screen has no state
18
+ * between not-touching and touching, so emulating hover would be a
19
+ * difference from the web rather than a match to it.
20
+ */
21
+ const PRESSED_SCALE = grytScaleSteps.button.press;
14
22
  export function Button({ children, tone = "primary", size = "medium", startIcon, endIcon, style, hasPopup = false, disabled, ...rest }) {
15
23
  const theme = useTheme();
16
24
  const reducedMotion = useReducedMotion();
17
- // useState with an initialiser rather than useRef(new Animated.Value(1)):
18
- // reading .current during render is a lint error, and the useRef form also
19
- // constructs a throwaway Animated.Value on every render.
20
- const [scale] = useState(() => new Animated.Value(1));
21
25
  const metrics = SIZES[size];
22
26
  const animate = !reducedMotion && !hasPopup && !disabled;
27
+ const press = usePressScale(PRESSED_SCALE, !animate);
23
28
  const background = tone === "primary"
24
29
  ? theme.color.accent
25
30
  : tone === "secondary"
@@ -38,12 +43,7 @@ export function Button({ children, tone = "primary", size = "medium", startIcon,
38
43
  : tone === "ghost"
39
44
  ? theme.color.muted
40
45
  : theme.color.text;
41
- const springTo = (value) => {
42
- if (!animate)
43
- return;
44
- Animated.spring(scale, { toValue: value, ...SPRING }).start();
45
- };
46
- return (_jsx(Animated.View, { style: [{ transform: [{ scale }] }, style], children: _jsxs(Pressable, { accessibilityRole: "button", accessibilityState: { disabled: !!disabled }, disabled: disabled, onPressIn: () => springTo(PRESSED_SCALE), onPressOut: () => springTo(1), style: {
46
+ return (_jsx(Animated.View, { style: [press.style, style], children: _jsxs(Pressable, { accessibilityRole: "button", accessibilityState: { disabled: !!disabled }, disabled: disabled, onPressIn: press.onPressIn, onPressOut: press.onPressOut, style: {
47
47
  minHeight: metrics.minHeight,
48
48
  paddingHorizontal: metrics.paddingH,
49
49
  flexDirection: "row",
@@ -11,17 +11,25 @@ declare function Trigger({ children, style }: {
11
11
  style?: StyleProp<ViewStyle>;
12
12
  }): import("react").JSX.Element;
13
13
  /**
14
- * Unmounted when closed, rather than animated to zero height.
14
+ * Animates its height, the same as the web.
15
15
  *
16
- * The web animates the panel's height, which needs the content measured first.
17
- * React Native can do it with `onLayout` plus `Animated`, and it costs a frame
18
- * of the content rendered at the wrong size before the measurement lands. Not
19
- * worth it for the first pass, and it is in the exceptions table.
16
+ * The web transitions height on `ease-spring` at `--gryt-dur-spring`, and this
17
+ * now does the same curve at the same duration.
18
+ *
19
+ * The content has to be measured before its height can be animated to, which
20
+ * is why this was unmounted-when-closed to begin with. It is measured off an
21
+ * absolutely positioned copy that never affects layout, so nothing is drawn at
22
+ * the wrong size on the way in — the earlier objection was to measuring the
23
+ * visible content and paying a frame for it.
24
+ *
25
+ * Children stay mounted while closed, at height zero with `overflow: hidden`,
26
+ * matching the web. That is a behaviour change as well as a visual one: state
27
+ * inside a closed panel now survives, where before it was destroyed.
20
28
  */
21
29
  declare function Panel({ children, style }: {
22
30
  children?: ReactNode;
23
31
  style?: StyleProp<ViewStyle>;
24
- }): import("react").JSX.Element | null;
32
+ }): import("react").JSX.Element;
25
33
  export declare const Collapsible: {
26
34
  Root: typeof Root;
27
35
  Trigger: typeof Trigger;
@@ -1 +1 @@
1
- {"version":3,"file":"Collapsible.d.ts","sourceRoot":"","sources":["../../../src/components/Collapsible/Collapsible.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAClE,OAAO,EAAmB,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAE/E,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAe/E,MAAM,WAAW,oBAAqB,SAAQ,cAAc;IAC1D,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED,iBAAS,IAAI,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,SAAS,EAAE,EAAE,oBAAoB,+BAOpE;AAED,iBAAS,OAAO,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;IAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;IAAC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;CAAE,+BAY3F;AAED;;;;;;;GAOG;AACH,iBAAS,KAAK,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;IAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;IAAC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;CAAE,sCAIzF;AAED,eAAO,MAAM,WAAW;;;;CAA2B,CAAC"}
1
+ {"version":3,"file":"Collapsible.d.ts","sourceRoot":"","sources":["../../../src/components/Collapsible/Collapsible.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAkD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACvF,OAAO,EAAmB,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAK/E,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAe/E,MAAM,WAAW,oBAAqB,SAAQ,cAAc;IAC1D,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED,iBAAS,IAAI,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,SAAS,EAAE,EAAE,oBAAoB,+BAOpE;AAED,iBAAS,OAAO,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;IAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;IAAC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;CAAE,+BAY3F;AAED;;;;;;;;;;;;;;;GAeG;AACH,iBAAS,KAAK,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;IAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;IAAC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;CAAE,+BA+BzF;AAED,eAAO,MAAM,WAAW;;;;CAA2B,CAAC"}
@@ -1,6 +1,9 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { createContext, useContext } from "react";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createContext, useContext, useEffect, useState } from "react";
3
3
  import { Pressable, View } from "react-native";
4
+ import Animated, { useAnimatedStyle, useSharedValue } from "react-native-reanimated";
5
+ import { useReducedMotion } from "../../hooks/useReducedMotion.js";
6
+ import { springy } from "../../motion/index.js";
4
7
  import { useOpenState } from "../../overlay/useOpenState.js";
5
8
  const CollapsibleContext = createContext(null);
6
9
  function useCollapsible(part) {
@@ -18,17 +21,35 @@ function Trigger({ children, style }) {
18
21
  return (_jsx(Pressable, { accessibilityRole: "button", accessibilityState: { expanded: open }, onPress: () => setOpen(!open), style: style, children: children }));
19
22
  }
20
23
  /**
21
- * Unmounted when closed, rather than animated to zero height.
24
+ * Animates its height, the same as the web.
22
25
  *
23
- * The web animates the panel's height, which needs the content measured first.
24
- * React Native can do it with `onLayout` plus `Animated`, and it costs a frame
25
- * of the content rendered at the wrong size before the measurement lands. Not
26
- * worth it for the first pass, and it is in the exceptions table.
26
+ * The web transitions height on `ease-spring` at `--gryt-dur-spring`, and this
27
+ * now does the same curve at the same duration.
28
+ *
29
+ * The content has to be measured before its height can be animated to, which
30
+ * is why this was unmounted-when-closed to begin with. It is measured off an
31
+ * absolutely positioned copy that never affects layout, so nothing is drawn at
32
+ * the wrong size on the way in — the earlier objection was to measuring the
33
+ * visible content and paying a frame for it.
34
+ *
35
+ * Children stay mounted while closed, at height zero with `overflow: hidden`,
36
+ * matching the web. That is a behaviour change as well as a visual one: state
37
+ * inside a closed panel now survives, where before it was destroyed.
27
38
  */
28
39
  function Panel({ children, style }) {
29
40
  const { open } = useCollapsible("Panel");
30
- if (!open)
31
- return null;
32
- return _jsx(View, { style: style, children: children });
41
+ const reducedMotion = useReducedMotion();
42
+ const [height, setHeight] = useState(0);
43
+ const progress = useSharedValue(open ? 1 : 0);
44
+ useEffect(() => {
45
+ const to = open ? 1 : 0;
46
+ // eslint-disable-next-line react-hooks/immutability
47
+ progress.value = reducedMotion ? to : springy(to);
48
+ }, [open, progress, reducedMotion]);
49
+ const animated = useAnimatedStyle(() => ({
50
+ height: height * progress.value,
51
+ opacity: progress.value
52
+ }));
53
+ return (_jsxs(Animated.View, { style: [{ overflow: "hidden" }, animated, style], children: [_jsx(View, { pointerEvents: "none", style: { position: "absolute", left: 0, right: 0, opacity: 0 }, onLayout: (e) => setHeight(e.nativeEvent.layout.height), children: children }), children] }));
33
54
  }
34
55
  export const Collapsible = { Root, Trigger, Panel };
@@ -1 +1 @@
1
- {"version":3,"file":"Dialog.d.ts","sourceRoot":"","sources":["../../../src/components/Dialog/Dialog.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAClE,OAAO,EAML,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,4BAA4B,CAAC;AA2B/E,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,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED,iBAAS,OAAO,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,kBAAkB,+BAOvD;AAED;;;;;;GAMG;AACH,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,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,gEAAgE;IAChE,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,iBAAS,KAAK,CAAC,EACb,QAAQ,EACR,KAAK,EACL,WAAkB,EAClB,UAAiB,GAClB,EAAE,gBAAgB,+BA+DlB;AAED;;;;;;GAMG;AACH,iBAAS,QAAQ,IAAI,IAAI,CAExB;AAED,iBAAS,KAAK,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;IAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;IAAC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;CAAE,+BAUzF;AAED,iBAAS,WAAW,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;IAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;IAAC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;CAAE,+BAO/F;AAED,iBAAS,MAAM,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;IAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;IAAC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;CAAE,+BAY1F;AAED,iBAAS,KAAK,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;IAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;IAAC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;CAAE,+BAOzF;AAED,eAAO,MAAM,MAAM;;;;;;;;;;CAUlB,CAAC"}
1
+ {"version":3,"file":"Dialog.d.ts","sourceRoot":"","sources":["../../../src/components/Dialog/Dialog.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAClE,OAAO,EAML,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,4BAA4B,CAAC;AA2B/E,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,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED,iBAAS,OAAO,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,kBAAkB,+BAOvD;AAED;;;;;;GAMG;AACH,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,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,gEAAgE;IAChE,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,iBAAS,KAAK,CAAC,EACb,QAAQ,EACR,KAAK,EACL,WAAkB,EAClB,UAAiB,GAClB,EAAE,gBAAgB,+BAwFlB;AAED;;;;;;GAMG;AACH,iBAAS,QAAQ,IAAI,IAAI,CAExB;AAED,iBAAS,KAAK,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;IAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;IAAC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;CAAE,+BAUzF;AAED,iBAAS,WAAW,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;IAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;IAAC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;CAAE,+BAO/F;AAED,iBAAS,MAAM,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;IAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;IAAC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;CAAE,+BAY1F;AAED,iBAAS,KAAK,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;IAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;IAAC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;CAAE,+BAOzF;AAED,eAAO,MAAM,MAAM;;;;;;;;;;CAUlB,CAAC"}
@@ -42,7 +42,10 @@ function Popup({ children, style, dismissible = true, scrollable = true, }) {
42
42
  {
43
43
  width: "100%",
44
44
  maxWidth: 480,
45
- maxHeight: "80%",
45
+ // Shrinks inside the capped wrapper below rather than carrying its
46
+ // own percentage, so the popup hugs short content and gives way when
47
+ // the content is taller than the cap.
48
+ flexShrink: 1,
46
49
  backgroundColor: theme.color.surfaceRaised,
47
50
  borderRadius: theme.radius.lg,
48
51
  borderWidth: 1,
@@ -51,7 +54,17 @@ function Popup({ children, style, dismissible = true, scrollable = true, }) {
51
54
  gap: theme.space(3),
52
55
  },
53
56
  style,
54
- ], children: scrollable ? (_jsx(ScrollView, { contentContainerStyle: { gap: theme.space(3) }, children: children })) : (children) }));
57
+ ], children: scrollable ? (
58
+ // flexGrow 0 so the ScrollView takes the height of its content rather
59
+ // than filling the popup, and flexShrink 1 so it still gives way when
60
+ // maxHeight caps it and the content has to scroll.
61
+ //
62
+ // Without these it clipped every short dialog: a ScrollView has no
63
+ // intrinsic height, the popup sizes to its content, and neither had
64
+ // anything to measure against — so the footer was drawn half off the
65
+ // bottom. The `scrollable={false}` path was unaffected, which is what
66
+ // made it look like a layout choice rather than a bug (GRYT-379).
67
+ _jsx(ScrollView, { style: { flexGrow: 0, flexShrink: 1 }, contentContainerStyle: { gap: theme.space(3) }, children: children })) : (children) }));
55
68
  return (_jsx(Modal, { visible: open, transparent: true, animationType: "fade",
56
69
  // Android's hardware back button, which is the closest thing a phone has
57
70
  // to the Escape key Base UI listens for. AlertDialog turns it off for the
@@ -68,7 +81,7 @@ function Popup({ children, style, dismissible = true, scrollable = true, }) {
68
81
  justifyContent: "center",
69
82
  padding: theme.space(5),
70
83
  backgroundColor: "rgba(0,0,0,0.6)",
71
- }, children: _jsx(Pressable, { onPress: () => { }, style: { width: "100%", alignItems: "center" }, children: body }) }) }));
84
+ }, children: _jsx(Pressable, { onPress: () => { }, style: { width: "100%", alignItems: "center", maxHeight: "80%" }, children: body }) }) }));
72
85
  }
73
86
  /**
74
87
  * Not rendered.
@@ -28,7 +28,7 @@ export interface DrawerPopupProps {
28
28
  * panel, so this one animates even though Collapsible and Skeleton do not.
29
29
  * Reduce-motion still turns it off: the drawer arrives in place instead.
30
30
  */
31
- declare function Popup({ children, side, size, dismissible, style, }: DrawerPopupProps): import("react").JSX.Element;
31
+ declare function Popup({ children, side, size, dismissible, style }: DrawerPopupProps): import("react").JSX.Element;
32
32
  declare function Close({ children, style }: {
33
33
  children?: ReactNode;
34
34
  style?: StyleProp<ViewStyle>;
@@ -1 +1 @@
1
- {"version":3,"file":"Drawer.d.ts","sourceRoot":"","sources":["../../../src/components/Drawer/Drawer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAkD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACvF,OAAO,EAKL,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAG/E,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAerD,MAAM,WAAW,eAAgB,SAAQ,cAAc;IACrD,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,iBAAS,IAAI,CAAC,EAAE,QAAQ,EAAE,GAAG,SAAS,EAAE,EAAE,eAAe,+BAGxD;AAED,iBAAS,OAAO,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;IAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;IAAC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;CAAE,+BAO3F;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;AAED;;;;;;GAMG;AACH,iBAAS,KAAK,CAAC,EACb,QAAQ,EACR,IAAa,EACb,IAAU,EACV,WAAkB,EAClB,KAAK,GACN,EAAE,gBAAgB,+BAuElB;AAED,iBAAS,KAAK,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;IAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;IAAC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;CAAE,+BAOzF;AAED,eAAO,MAAM,MAAM;;;;;;CAA0C,CAAC"}
1
+ {"version":3,"file":"Drawer.d.ts","sourceRoot":"","sources":["../../../src/components/Drawer/Drawer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAQtB,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;AAED;;;;;;GAMG;AACH,iBAAS,KAAK,CAAC,EACb,QAAQ,EACR,IAAa,EACb,IAAU,EACV,WAAkB,EAClB,KAAK,EACN,EAAE,gBAAgB,+BA0ElB;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,7 +1,9 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { createContext, useContext, useEffect, useState } from "react";
3
- import { Animated, Dimensions, Modal, Pressable, } from "react-native";
2
+ import { createContext, useContext, useEffect } from "react";
3
+ import { Dimensions, Modal, Pressable } from "react-native";
4
+ import Animated, { useAnimatedStyle, useSharedValue } from "react-native-reanimated";
4
5
  import { useReducedMotion } from "../../hooks/useReducedMotion.js";
6
+ import { durations, springy } from "../../motion/index.js";
5
7
  import { useOpenState } from "../../overlay/useOpenState.js";
6
8
  import { useTheme } from "../../theme/index.js";
7
9
  const DrawerContext = createContext(null);
@@ -13,7 +15,7 @@ function useDrawer(part) {
13
15
  }
14
16
  function Root({ children, ...openProps }) {
15
17
  const state = useOpenState(openProps);
16
- return _jsx(DrawerContext.Provider, { value: state, children: children });
18
+ return (_jsx(DrawerContext.Provider, { value: state, children: children }));
17
19
  }
18
20
  function Trigger({ children, style }) {
19
21
  const { setOpen } = useDrawer("Trigger");
@@ -29,52 +31,55 @@ function Portal({ children }) {
29
31
  * panel, so this one animates even though Collapsible and Skeleton do not.
30
32
  * Reduce-motion still turns it off: the drawer arrives in place instead.
31
33
  */
32
- function Popup({ children, side = "left", size = 0.8, dismissible = true, style, }) {
34
+ function Popup({ children, side = "left", size = 0.8, dismissible = true, style }) {
33
35
  const { open, setOpen } = useDrawer("Popup");
34
36
  const theme = useTheme();
35
37
  const reducedMotion = useReducedMotion();
36
38
  const screen = Dimensions.get("window");
37
39
  const vertical = side === "bottom";
38
40
  const extent = vertical ? screen.height * size : screen.width * size;
39
- const [progress] = useState(() => new Animated.Value(0));
41
+ const progress = useSharedValue(0);
42
+ const hidden = side === "right" ? extent : side === "left" ? -extent : extent;
43
+ // 700ms, the web's `--gryt-dur-spring-soft`, on the overshooting curve —
44
+ // which is what the web uses here despite the panel travelling its own
45
+ // width. Matching it rather than substituting the tight curve, because the
46
+ // brief is 1:1 with the web and not a second opinion about it.
40
47
  useEffect(() => {
41
- if (!open) {
42
- progress.setValue(0);
43
- return;
44
- }
45
- if (reducedMotion) {
46
- progress.setValue(1);
47
- return;
48
- }
49
- Animated.spring(progress, {
50
- toValue: 1,
51
- useNativeDriver: true,
52
- speed: 20,
53
- bounciness: 0,
54
- }).start();
48
+ // eslint-disable-next-line react-hooks/immutability
49
+ progress.value =
50
+ !open || reducedMotion
51
+ ? open
52
+ ? 1
53
+ : 0
54
+ : springy(1, { duration: durations.springSoft });
55
55
  }, [open, progress, reducedMotion]);
56
- const hidden = side === "right" ? extent : side === "left" ? -extent : extent;
57
- const translate = progress.interpolate({
58
- inputRange: [0, 1],
59
- outputRange: [hidden, 0],
60
- });
61
- return (_jsx(Modal, { visible: open, transparent: true, animationType: "none", onRequestClose: dismissible ? () => setOpen(false) : undefined, children: _jsx(Pressable, { onPress: dismissible ? () => setOpen(false) : undefined, style: { flex: 1, backgroundColor: "rgba(0,0,0,0.6)" }, children: _jsx(Animated.View, { accessibilityViewIsModal: true, style: {
62
- position: "absolute",
63
- top: side === "bottom" ? undefined : 0,
64
- bottom: 0,
65
- left: side === "right" ? undefined : 0,
66
- right: side === "left" ? undefined : 0,
67
- width: vertical ? "100%" : extent,
68
- height: vertical ? extent : "100%",
69
- backgroundColor: theme.color.surface,
70
- borderColor: theme.color.border,
71
- borderRightWidth: side === "left" ? 1 : 0,
72
- borderLeftWidth: side === "right" ? 1 : 0,
73
- borderTopWidth: vertical ? 1 : 0,
74
- borderTopLeftRadius: vertical ? theme.radius.lg : 0,
75
- borderTopRightRadius: vertical ? theme.radius.lg : 0,
76
- transform: [vertical ? { translateY: translate } : { translateX: translate }],
77
- }, children: _jsx(Pressable, { onPress: () => { }, style: [{ flex: 1 }, style], children: children }) }) }) }));
56
+ const panelStyle = useAnimatedStyle(() => ({
57
+ transform: [
58
+ vertical
59
+ ? { translateY: hidden + (0 - hidden) * progress.value }
60
+ : { translateX: hidden + (0 - hidden) * progress.value }
61
+ ],
62
+ opacity: progress.value
63
+ }));
64
+ return (_jsx(Modal, { visible: open, transparent: true, animationType: "none", onRequestClose: dismissible ? () => setOpen(false) : undefined, children: _jsx(Pressable, { onPress: dismissible ? () => setOpen(false) : undefined, style: { flex: 1, backgroundColor: "rgba(0,0,0,0.6)" }, children: _jsx(Animated.View, { accessibilityViewIsModal: true, style: [
65
+ {
66
+ position: "absolute",
67
+ top: side === "bottom" ? undefined : 0,
68
+ bottom: 0,
69
+ left: side === "right" ? undefined : 0,
70
+ right: side === "left" ? undefined : 0,
71
+ width: vertical ? "100%" : extent,
72
+ height: vertical ? extent : "100%",
73
+ backgroundColor: theme.color.surface,
74
+ borderColor: theme.color.border,
75
+ borderRightWidth: side === "left" ? 1 : 0,
76
+ borderLeftWidth: side === "right" ? 1 : 0,
77
+ borderTopWidth: vertical ? 1 : 0,
78
+ borderTopLeftRadius: vertical ? theme.radius.lg : 0,
79
+ borderTopRightRadius: vertical ? theme.radius.lg : 0
80
+ },
81
+ panelStyle
82
+ ], children: _jsx(Pressable, { onPress: () => { }, style: [{ flex: 1 }, style], children: children }) }) }) }));
78
83
  }
79
84
  function Close({ children, style }) {
80
85
  const { setOpen } = useDrawer("Close");
@@ -6,13 +6,24 @@ export interface ProgressProps {
6
6
  style?: StyleProp<ViewStyle>;
7
7
  }
8
8
  /**
9
- * Determinate only for now.
9
+ * Determinate only and so is the web, which is not what this used to say.
10
10
  *
11
- * The web's indeterminate state is a CSS keyframe sweeping a partial bar across
12
- * the track. Reproducing it here needs an Animated loop and a reduce-motion
13
- * check, which is worth doing properly rather than as a footnote on this one, so
14
- * an undefined value currently renders an empty track. Recorded in the parity
15
- * exceptions table.
11
+ * The previous note here claimed the web's indeterminate state was a CSS
12
+ * keyframe sweeping a bar across the track, and listed this as a parity
13
+ * exception. It is not one. `@gryt/ui`'s Progress passes `value={null}` to
14
+ * Base UI and styles nothing for it: there is no `@keyframes` anywhere in the
15
+ * package, no rule for `data-indeterminate`, and the compiled stylesheet has
16
+ * zero rules matching `gryt-progress`. An indeterminate Progress renders an
17
+ * empty track on the web too.
18
+ *
19
+ * So both platforms agree, and both are missing the feature. Implementing it
20
+ * belongs on both at once — GRYT-382 — rather than here, where it would make
21
+ * React Native the one that behaves differently.
22
+ *
23
+ * The travel that *is* implemented follows the web deliberately: 300ms on
24
+ * ease-out rather than the spring. A progress bar is a reading, and the
25
+ * spring's 12% overshoot on a jump to 100% runs past the end of the track and
26
+ * comes back — which reads as the job finishing, unfinishing, then finishing.
16
27
  */
17
28
  export declare function Progress({ value, tone, style }: ProgressProps): import("react").JSX.Element;
18
29
  //# sourceMappingURL=Progress.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Progress.d.ts","sourceRoot":"","sources":["../../../src/components/Progress/Progress.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAIpE,MAAM,WAAW,aAAa;IAC5B,0DAA0D;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;IACnD,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,EAAE,KAAK,EAAE,IAAe,EAAE,KAAK,EAAE,EAAE,aAAa,+BA4BxE"}
1
+ {"version":3,"file":"Progress.d.ts","sourceRoot":"","sources":["../../../src/components/Progress/Progress.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAIpE,MAAM,WAAW,aAAa;IAC5B,0DAA0D;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;IACnD,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,QAAQ,CAAC,EAAE,KAAK,EAAE,IAAe,EAAE,KAAK,EAAE,EAAE,aAAa,+BA4BxE"}
@@ -2,13 +2,24 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { View } from "react-native";
3
3
  import { useTheme } from "../../theme/index.js";
4
4
  /**
5
- * Determinate only for now.
5
+ * Determinate only and so is the web, which is not what this used to say.
6
6
  *
7
- * The web's indeterminate state is a CSS keyframe sweeping a partial bar across
8
- * the track. Reproducing it here needs an Animated loop and a reduce-motion
9
- * check, which is worth doing properly rather than as a footnote on this one, so
10
- * an undefined value currently renders an empty track. Recorded in the parity
11
- * exceptions table.
7
+ * The previous note here claimed the web's indeterminate state was a CSS
8
+ * keyframe sweeping a bar across the track, and listed this as a parity
9
+ * exception. It is not one. `@gryt/ui`'s Progress passes `value={null}` to
10
+ * Base UI and styles nothing for it: there is no `@keyframes` anywhere in the
11
+ * package, no rule for `data-indeterminate`, and the compiled stylesheet has
12
+ * zero rules matching `gryt-progress`. An indeterminate Progress renders an
13
+ * empty track on the web too.
14
+ *
15
+ * So both platforms agree, and both are missing the feature. Implementing it
16
+ * belongs on both at once — GRYT-382 — rather than here, where it would make
17
+ * React Native the one that behaves differently.
18
+ *
19
+ * The travel that *is* implemented follows the web deliberately: 300ms on
20
+ * ease-out rather than the spring. A progress bar is a reading, and the
21
+ * spring's 12% overshoot on a jump to 100% runs past the end of the track and
22
+ * comes back — which reads as the job finishing, unfinishing, then finishing.
12
23
  */
13
24
  export function Progress({ value, tone = "accent", style }) {
14
25
  const theme = useTheme();
@@ -1 +1 @@
1
- {"version":3,"file":"Slider.d.ts","sourceRoot":"","sources":["../../../src/components/Slider/Slider.tsx"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAsB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAErE,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,aAAa,EAAE,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC,CAAC;AAEhG,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,+DAA+D;IAC/D,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAKD;;;;;;;;;GASG;AACH,wBAAgB,MAAM,CAAC,EACrB,KAAK,EAAE,UAAU,EACjB,YAAgB,EAChB,aAAa,EACb,aAAa,EACb,GAAO,EACP,GAAS,EACT,IAAQ,EACR,QAAQ,EACR,IAAgB,EAChB,KAAK,EACL,kBAAkB,GACnB,EAAE,WAAW,+BA0Gb"}
1
+ {"version":3,"file":"Slider.d.ts","sourceRoot":"","sources":["../../../src/components/Slider/Slider.tsx"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAMtB,OAAO,EAAsB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAGrE,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,aAAa,EAAE,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC,CAAC;AAEhG,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,+DAA+D;IAC/D,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAKD;;;;;;;;;GASG;AACH,wBAAgB,MAAM,CAAC,EACrB,KAAK,EAAE,UAAU,EACjB,YAAgB,EAChB,aAAa,EACb,aAAa,EACb,GAAO,EACP,GAAS,EACT,IAAQ,EACR,QAAQ,EACR,IAAgB,EAChB,KAAK,EACL,kBAAkB,GACnB,EAAE,WAAW,+BAsIb"}
@@ -1,7 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from "react";
3
3
  import { PanResponder, View, } from "react-native";
4
+ import Animated, { useAnimatedStyle, useSharedValue } from "react-native-reanimated";
5
+ import { grytScaleSteps } from "@gryt/theme";
6
+ import { springy } from "../../motion/index.js";
4
7
  import { toneRamp, useTheme } from "../../theme/index.js";
8
+ import { valueAt } from "./sliderValue.js";
5
9
  const TRACK_HEIGHT = 4;
6
10
  const THUMB = 20;
7
11
  /**
@@ -27,6 +31,16 @@ export function Slider({ value: controlled, defaultValue = 0, onValueChange, onV
27
31
  // fire from touch events, which is always after an effect has run.
28
32
  const state = useRef({ width, value, min, max, step, disabled });
29
33
  const emit = useRef({ onValueChange, onValueCommit, controlled });
34
+ /** Where the current gesture started, in track coordinates. */
35
+ const origin = useRef(0);
36
+ /**
37
+ * `active:scale-[0.94]` on the web. `hover:scale-[1.12]` has no touch
38
+ * equivalent, so only the press half ports.
39
+ */
40
+ const thumbScale = useSharedValue(1);
41
+ const thumbStyle = useAnimatedStyle(() => ({
42
+ transform: [{ scale: thumbScale.value }]
43
+ }));
30
44
  useEffect(() => {
31
45
  state.current = { width, value, min, max, step, disabled };
32
46
  }, [width, value, min, max, step, disabled]);
@@ -35,12 +49,11 @@ export function Slider({ value: controlled, defaultValue = 0, onValueChange, onV
35
49
  }, [onValueChange, onValueCommit, controlled]);
36
50
  const valueFromX = (x) => {
37
51
  const s = state.current;
52
+ // Before layout there is no position to read, so hold what we have rather
53
+ // than snapping to min.
38
54
  if (s.width <= 0)
39
55
  return s.value;
40
- const ratio = Math.max(0, Math.min(1, x / s.width));
41
- const raw = s.min + ratio * (s.max - s.min);
42
- const stepped = Math.round(raw / s.step) * s.step;
43
- return Math.max(s.min, Math.min(s.max, stepped));
56
+ return valueAt(x, s);
44
57
  };
45
58
  const apply = (next) => {
46
59
  if (emit.current.controlled === undefined)
@@ -62,13 +75,29 @@ export function Slider({ value: controlled, defaultValue = 0, onValueChange, onV
62
75
  // Claimed on move as well, so a drag that begins as a scroll still lands
63
76
  // here once it is clearly horizontal.
64
77
  onMoveShouldSetPanResponder: () => !state.current.disabled,
65
- onPanResponderGrant: (e) => apply(valueFromX(e.nativeEvent.locationX)),
78
+ onPanResponderGrant: (e) => {
79
+ // Where the finger landed, kept for the drag to offset from.
80
+ origin.current = e.nativeEvent.locationX;
81
+ thumbScale.value = springy(grytScaleSteps.sliderThumb.press);
82
+ apply(valueFromX(e.nativeEvent.locationX));
83
+ },
84
+ // `gesture.dx` is the distance from where the gesture *started*, not from
85
+ // the previous event. Offsetting the live value by it therefore adds the
86
+ // whole travel again on every move, and the thumb accelerates away from
87
+ // the finger — 200px wide, 0-100, dragging to x=50 then 60 then 70 gave
88
+ // 25, then 55, then 90. Tapping was always fine, because grant uses
89
+ // locationX directly, which is what made it look like a rendering
90
+ // problem rather than an arithmetic one.
91
+ //
92
+ // Anchoring on the grant position is also what makes the drag continue
93
+ // from the finger: grant has already seeked the thumb there.
66
94
  onPanResponderMove: (_e, gesture) => {
67
- const s = state.current;
68
- const ratio = (s.value - s.min) / (s.max - s.min || 1);
69
- apply(valueFromX(ratio * s.width + gesture.dx));
95
+ apply(valueFromX(origin.current + gesture.dx));
96
+ },
97
+ onPanResponderRelease: () => {
98
+ thumbScale.value = springy(1);
99
+ emit.current.onValueCommit?.(state.current.value);
70
100
  },
71
- onPanResponderRelease: () => emit.current.onValueCommit?.(state.current.value),
72
101
  }));
73
102
  /* eslint-enable react-hooks/refs */
74
103
  const ratio = (value - min) / (max - min || 1);
@@ -81,14 +110,17 @@ export function Slider({ value: controlled, defaultValue = 0, onValueChange, onV
81
110
  height: "100%",
82
111
  borderRadius: TRACK_HEIGHT / 2,
83
112
  backgroundColor: ramp[8],
84
- } }) }), _jsx(View, { pointerEvents: "none", style: {
85
- position: "absolute",
86
- left: Math.max(0, ratio * width - THUMB / 2),
87
- width: THUMB,
88
- height: THUMB,
89
- borderRadius: THUMB / 2,
90
- backgroundColor: ramp[8],
91
- borderWidth: 2,
92
- borderColor: theme.color.bg,
93
- } })] }));
113
+ } }) }), _jsx(Animated.View, { pointerEvents: "none", style: [
114
+ {
115
+ position: "absolute",
116
+ left: Math.max(0, ratio * width - THUMB / 2),
117
+ width: THUMB,
118
+ height: THUMB,
119
+ borderRadius: THUMB / 2,
120
+ backgroundColor: ramp[8],
121
+ borderWidth: 2,
122
+ borderColor: theme.color.bg,
123
+ },
124
+ thumbStyle,
125
+ ] })] }));
94
126
  }
@@ -0,0 +1,15 @@
1
+ export interface SliderScale {
2
+ /** Track width in pixels. Zero or less means the layout has not happened. */
3
+ width: number;
4
+ min: number;
5
+ max: number;
6
+ step: number;
7
+ }
8
+ /**
9
+ * The value at `x` pixels along the track.
10
+ *
11
+ * Clamped at both ends, snapped to `step`, and safe to call before layout —
12
+ * a zero width returns `min`, since there is no position to read yet.
13
+ */
14
+ export declare function valueAt(x: number, scale: SliderScale): number;
15
+ //# sourceMappingURL=sliderValue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sliderValue.d.ts","sourceRoot":"","sources":["../../../src/components/Slider/sliderValue.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,WAAW;IAC1B,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,MAAM,CAU7D"}
@@ -0,0 +1,23 @@
1
+ /* Track position to value, as a pure function.
2
+ *
3
+ * Split out of Slider.tsx for the same reason placePopup.ts is split out of the
4
+ * overlay: it is arithmetic, it is where the bugs are, and a closure over a ref
5
+ * cannot be tested. GRYT-378 was a one-line error in this maths that shipped,
6
+ * passed every check, and was only caught by a person dragging a real thumb on
7
+ * a real phone.
8
+ */
9
+ /**
10
+ * The value at `x` pixels along the track.
11
+ *
12
+ * Clamped at both ends, snapped to `step`, and safe to call before layout —
13
+ * a zero width returns `min`, since there is no position to read yet.
14
+ */
15
+ export function valueAt(x, scale) {
16
+ const { width, min, max, step } = scale;
17
+ if (width <= 0)
18
+ return min;
19
+ const ratio = Math.max(0, Math.min(1, x / width));
20
+ const raw = min + ratio * (max - min);
21
+ const stepped = step > 0 ? Math.round(raw / step) * step : raw;
22
+ return Math.max(min, Math.min(max, stepped));
23
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"Switch.d.ts","sourceRoot":"","sources":["../../../src/components/Switch/Switch.tsx"],"names":[],"mappings":"AACA,OAAO,EAAuB,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAKnF,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC;AAExE,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IAC7C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAQD;;;;;;;;;;;GAWG;AACH,wBAAgB,MAAM,CAAC,EACrB,OAAO,EAAE,UAAU,EACnB,cAAsB,EACtB,eAAe,EACf,QAAQ,EACR,IAAgB,EAChB,KAAK,EACL,kBAAkB,GACnB,EAAE,WAAW,+BA0Db"}
1
+ {"version":3,"file":"Switch.d.ts","sourceRoot":"","sources":["../../../src/components/Switch/Switch.tsx"],"names":[],"mappings":"AACA,OAAO,EAAa,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAOzE,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC;AAExE,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IAC7C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAQD;;;;;;;;;;;GAWG;AACH,wBAAgB,MAAM,CAAC,EACrB,OAAO,EAAE,UAAU,EACnB,cAAsB,EACtB,eAAe,EACf,QAAQ,EACR,IAAgB,EAChB,KAAK,EACL,kBAAkB,GACnB,EAAE,WAAW,+BA6Db"}
@@ -1,7 +1,9 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from "react";
3
- import { Animated, Pressable } from "react-native";
3
+ import { Pressable } from "react-native";
4
+ import Animated, { useAnimatedStyle, useSharedValue } from "react-native-reanimated";
4
5
  import { useReducedMotion } from "../../hooks/useReducedMotion.js";
6
+ import { springy } from "../../motion/index.js";
5
7
  import { toneRamp, useTheme } from "../../theme/index.js";
6
8
  // The web is h-6 w-10 with a thumb translating x-4. Same numbers in points.
7
9
  const TRACK_WIDTH = 40;
@@ -25,19 +27,19 @@ export function Switch({ checked: controlled, defaultChecked = false, onCheckedC
25
27
  const reducedMotion = useReducedMotion();
26
28
  const [uncontrolled, setUncontrolled] = useState(defaultChecked);
27
29
  const checked = controlled ?? uncontrolled;
28
- const [offset] = useState(() => new Animated.Value(checked ? TRAVEL : 0));
30
+ const offset = useSharedValue(checked ? TRAVEL : 0);
31
+ // The web is `translate-x-4` on `ease-spring` — the overshooting curve, even
32
+ // though the thumb travels, because the travel is 16px inside a 40px track
33
+ // rather than the width of a container. Following the web rather than
34
+ // second-guessing it: 1:1 means the same curve, not a better-argued one.
29
35
  useEffect(() => {
30
- if (reducedMotion) {
31
- offset.setValue(checked ? TRAVEL : 0);
32
- return;
33
- }
34
- Animated.spring(offset, {
35
- toValue: checked ? TRAVEL : 0,
36
- useNativeDriver: true,
37
- speed: 40,
38
- bounciness: 4,
39
- }).start();
36
+ const to = checked ? TRAVEL : 0;
37
+ // eslint-disable-next-line react-hooks/immutability
38
+ offset.value = reducedMotion ? to : springy(to);
40
39
  }, [checked, offset, reducedMotion]);
40
+ const thumbStyle = useAnimatedStyle(() => ({
41
+ transform: [{ translateX: offset.value }]
42
+ }));
41
43
  const ramp = toneRamp(theme, tone);
42
44
  return (_jsx(Pressable, { accessibilityRole: "switch", accessibilityState: { checked, disabled: !!disabled }, accessibilityLabel: accessibilityLabel, disabled: disabled, onPress: () => {
43
45
  const next = !checked;
@@ -55,11 +57,13 @@ export function Switch({ checked: controlled, defaultChecked = false, onCheckedC
55
57
  opacity: disabled ? 0.5 : 1,
56
58
  },
57
59
  style,
58
- ], children: _jsx(Animated.View, { style: {
59
- width: THUMB,
60
- height: THUMB,
61
- borderRadius: THUMB / 2,
62
- backgroundColor: checked ? theme.color.bg : theme.color.text,
63
- transform: [{ translateX: offset }],
64
- } }) }));
60
+ ], children: _jsx(Animated.View, { style: [
61
+ {
62
+ width: THUMB,
63
+ height: THUMB,
64
+ borderRadius: THUMB / 2,
65
+ backgroundColor: checked ? theme.color.bg : theme.color.text,
66
+ },
67
+ thumbStyle,
68
+ ] }) }));
65
69
  }
@@ -1 +1 @@
1
- {"version":3,"file":"Toast.d.ts","sourceRoot":"","sources":["../../../src/components/Toast/Toast.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAuE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC5G,OAAO,EAAmC,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAK/F,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAErE,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAMD,UAAU,iBAAiB;IACzB,IAAI,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,MAAM,CAAC;IACtC,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/B;AAID;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,CAAC,EAAE,SAAS,CAAA;CAAE,+BAoBnE;AAED,wBAAgB,QAAQ,IAAI,iBAAiB,CAI5C;AAmGD,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B"}
1
+ {"version":3,"file":"Toast.d.ts","sourceRoot":"","sources":["../../../src/components/Toast/Toast.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAuE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC5G,OAAO,EAAyB,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAQrF,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAErE,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAMD,UAAU,iBAAiB;IACzB,IAAI,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,MAAM,CAAC;IACtC,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/B;AAID;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,CAAC,EAAE,SAAS,CAAA;CAAE,+BAoBnE;AAED,wBAAgB,QAAQ,IAAI,iBAAiB,CAI5C;AAuGD,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B"}
@@ -1,7 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
3
- import { Animated, Pressable, Text, View } from "react-native";
3
+ import { Pressable, Text, View } from "react-native";
4
+ import Animated, { useAnimatedStyle, useSharedValue } from "react-native-reanimated";
5
+ import { grytScaleSteps } from "@gryt/theme";
4
6
  import { useReducedMotion } from "../../hooks/useReducedMotion.js";
7
+ import { fade, usePressScale } from "../../motion/index.js";
5
8
  import { useTheme } from "../../theme/index.js";
6
9
  const ToastContext = createContext(null);
7
10
  /**
@@ -57,23 +60,24 @@ function Viewport({ toasts, onDismiss, }) {
57
60
  function ToastItem({ toast, onDismiss, }) {
58
61
  const theme = useTheme();
59
62
  const reducedMotion = useReducedMotion();
60
- const [opacity] = useState(() => new Animated.Value(reducedMotion ? 1 : 0));
63
+ const opacity = useSharedValue(reducedMotion ? 1 : 0);
61
64
  const ramp = theme.scales[RAMP[toast.severity ?? "info"]];
62
65
  const duration = toast.duration === undefined ? 4000 : toast.duration;
66
+ // `active:scale-[0.96]` on the web. The `hover:scale-[1.04]` half has no
67
+ // touch equivalent and is deliberately not emulated.
68
+ const press = usePressScale(grytScaleSteps.toast.press, reducedMotion);
63
69
  useEffect(() => {
64
70
  if (!reducedMotion) {
65
- Animated.timing(opacity, {
66
- toValue: 1,
67
- duration: 150,
68
- useNativeDriver: true,
69
- }).start();
71
+ // eslint-disable-next-line react-hooks/immutability
72
+ opacity.value = fade(1);
70
73
  }
71
74
  if (duration === null)
72
75
  return;
73
76
  const timer = setTimeout(() => onDismiss(toast.id), duration);
74
77
  return () => clearTimeout(timer);
75
78
  }, [duration, onDismiss, opacity, reducedMotion, toast.id]);
76
- return (_jsx(Animated.View, { style: { opacity }, children: _jsxs(Pressable, { accessibilityRole: "alert", accessibilityLiveRegion: toast.severity === "error" || toast.severity === "warning" ? "assertive" : "polite", onPress: () => onDismiss(toast.id), style: {
79
+ const enter = useAnimatedStyle(() => ({ opacity: opacity.value }));
80
+ return (_jsx(Animated.View, { style: [enter, press.style], children: _jsxs(Pressable, { onPressIn: press.onPressIn, onPressOut: press.onPressOut, accessibilityRole: "alert", accessibilityLiveRegion: toast.severity === "error" || toast.severity === "warning" ? "assertive" : "polite", onPress: () => onDismiss(toast.id), style: {
77
81
  backgroundColor: theme.color.surfaceRaised,
78
82
  borderRadius: theme.radius.md,
79
83
  borderWidth: 1,
package/dist/index.d.ts CHANGED
@@ -36,4 +36,5 @@ export { AnchoredPopup, type AnchoredPopupProps } from "./overlay/AnchoredPopup.
36
36
  export { placePopup, type Align, type AnchorOptions, type AnchorRect, type PopupPlacement, type Side, } from "./overlay/placePopup.js";
37
37
  export { useOpenState, type OpenStateProps } from "./overlay/useOpenState.js";
38
38
  export { useReducedMotion } from "./hooks/useReducedMotion.js";
39
+ export { durations, easeSpring, easeSpringTight, fade, springy, travel, usePressScale, type PressScale } from "./motion/index.js";
39
40
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -36,3 +36,4 @@ export { AnchoredPopup } from "./overlay/AnchoredPopup.js";
36
36
  export { placePopup, } from "./overlay/placePopup.js";
37
37
  export { useOpenState } from "./overlay/useOpenState.js";
38
38
  export { useReducedMotion } from "./hooks/useReducedMotion.js";
39
+ export { durations, easeSpring, easeSpringTight, fade, springy, travel, usePressScale } from "./motion/index.js";
@@ -0,0 +1,28 @@
1
+ /** Same shape as Reanimated's `EasingFunction`, without importing it. */
2
+ export type Easing = (t: number) => number;
3
+ /**
4
+ * Turn a sample list into an easing.
5
+ *
6
+ * The body is a worklet so it runs on the UI thread — an easing evaluated on
7
+ * the JS thread would drop frames under exactly the load this exists to
8
+ * survive. `points` is a plain array, which worklets capture by value.
9
+ *
10
+ * The interpolation is written out rather than calling `sampleCurve` from
11
+ * @gryt/theme, and that is not a style choice. A worklet cannot synchronously
12
+ * call a JS-thread function: doing so throws
13
+ *
14
+ * [Worklets] Tried to synchronously call a Remote Function
15
+ *
16
+ * on the first frame of every animation. That shipped in 0.3.0, because the
17
+ * only thing exercising the UI thread in testing used React Native's own
18
+ * easing rather than this one.
19
+ *
20
+ * easing.test.ts asserts this stays identical to `sampleCurve`, so the
21
+ * duplication cannot drift from the definition the web renders from.
22
+ */
23
+ export declare function easingFromSamples(samples: readonly number[]): Easing;
24
+ /** Overshoots ~12%. For things that scale in place. */
25
+ export declare const easeSpring: Easing;
26
+ /** Critically damped, no overshoot. For things that travel inside bounds. */
27
+ export declare const easeSpringTight: Easing;
28
+ //# sourceMappingURL=easing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"easing.d.ts","sourceRoot":"","sources":["../../src/motion/easing.ts"],"names":[],"mappings":"AASA,yEAAyE;AACzE,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAkBpE;AAED,uDAAuD;AACvD,eAAO,MAAM,UAAU,QAAmC,CAAC;AAE3D,6EAA6E;AAC7E,eAAO,MAAM,eAAe,QAAwC,CAAC"}
@@ -0,0 +1,51 @@
1
+ /* The sampled-spring easings, with no Reanimated import.
2
+ *
3
+ * Split from motion.ts for the same reason sliderValue.ts is split from
4
+ * Slider.tsx: this is arithmetic, it is where a bug hid, and importing
5
+ * react-native-reanimated drags in react-native-worklets, which cannot resolve
6
+ * under vitest. A file that cannot be imported cannot be tested.
7
+ */
8
+ import { springSamples, springTightSamples } from "@gryt/theme";
9
+ /**
10
+ * Turn a sample list into an easing.
11
+ *
12
+ * The body is a worklet so it runs on the UI thread — an easing evaluated on
13
+ * the JS thread would drop frames under exactly the load this exists to
14
+ * survive. `points` is a plain array, which worklets capture by value.
15
+ *
16
+ * The interpolation is written out rather than calling `sampleCurve` from
17
+ * @gryt/theme, and that is not a style choice. A worklet cannot synchronously
18
+ * call a JS-thread function: doing so throws
19
+ *
20
+ * [Worklets] Tried to synchronously call a Remote Function
21
+ *
22
+ * on the first frame of every animation. That shipped in 0.3.0, because the
23
+ * only thing exercising the UI thread in testing used React Native's own
24
+ * easing rather than this one.
25
+ *
26
+ * easing.test.ts asserts this stays identical to `sampleCurve`, so the
27
+ * duplication cannot drift from the definition the web renders from.
28
+ */
29
+ export function easingFromSamples(samples) {
30
+ const points = [...samples];
31
+ return (t) => {
32
+ "worklet";
33
+ const count = points.length;
34
+ if (count === 0)
35
+ return t;
36
+ if (t <= 0)
37
+ return points[0];
38
+ if (t >= 1)
39
+ return points[count - 1];
40
+ const span = (count - 1) * t;
41
+ const index = Math.floor(span);
42
+ const rest = span - index;
43
+ const from = points[index];
44
+ const to = points[index + 1] ?? from;
45
+ return from + (to - from) * rest;
46
+ };
47
+ }
48
+ /** Overshoots ~12%. For things that scale in place. */
49
+ export const easeSpring = easingFromSamples(springSamples);
50
+ /** Critically damped, no overshoot. For things that travel inside bounds. */
51
+ export const easeSpringTight = easingFromSamples(springTightSamples);
@@ -0,0 +1,4 @@
1
+ export { durations, fade, springy, travel } from "./motion.js";
2
+ export { easeSpring, easeSpringTight, easingFromSamples, type Easing } from "./easing.js";
3
+ export { usePressScale, type PressScale } from "./usePressScale.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/motion/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,iBAAiB,EAAE,KAAK,MAAM,EAAE,MAAM,UAAU,CAAC;AACvF,OAAO,EAAE,aAAa,EAAE,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { durations, fade, springy, travel } from "./motion.js";
2
+ export { easeSpring, easeSpringTight, easingFromSamples } from "./easing.js";
3
+ export { usePressScale } from "./usePressScale.js";
@@ -0,0 +1,15 @@
1
+ import { type WithTimingConfig } from "react-native-reanimated";
2
+ import { easeSpring, easeSpringTight } from "./easing.js";
3
+ export { easeSpring, easeSpringTight };
4
+ export declare const durations: {
5
+ readonly spring: 500;
6
+ readonly springSoft: 700;
7
+ readonly fast: 150;
8
+ };
9
+ /** `withTiming` on the overshooting spring, at the standard duration. */
10
+ export declare function springy(to: number, config?: WithTimingConfig): number;
11
+ /** `withTiming` on the critically damped spring — travel that must stay in bounds. */
12
+ export declare function travel(to: number, config?: WithTimingConfig): number;
13
+ /** A colour or opacity change, which should not feel sprung. */
14
+ export declare function fade(to: number, config?: WithTimingConfig): number;
15
+ //# sourceMappingURL=motion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"motion.d.ts","sourceRoot":"","sources":["../../src/motion/motion.ts"],"names":[],"mappings":"AAYA,OAAO,EAAsB,KAAK,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEpF,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAEvD,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,CAAC;AAEvC,eAAO,MAAM,SAAS;;;;CAAgB,CAAC;AAEvC,yEAAyE;AACzE,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,gBAAgB,UAG5D;AAED,sFAAsF;AACtF,wBAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,gBAAgB,UAG3D;AAED,gEAAgE;AAChE,wBAAgB,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,gBAAgB,UAGzD"}
@@ -0,0 +1,32 @@
1
+ /* The web components' motion, on React Native.
2
+ *
3
+ * Deliberately not `withSpring`. The curve in @gryt/theme is a damped spring
4
+ * solved analytically and sampled — `theme.css` calls it "the real curve of a
5
+ * spring rather than a physics engine approximating one". Reanimated's
6
+ * `withSpring` is a physics engine, so using it would mean approximating an
7
+ * exact curve with the very thing that curve was chosen over, and then tuning
8
+ * damping constants until it looked close.
9
+ *
10
+ * `withTiming` plus an easing that interpolates the same samples at the same
11
+ * duration is both simpler and actually identical.
12
+ */
13
+ import { Easing, withTiming } from "react-native-reanimated";
14
+ import { grytDurations } from "@gryt/theme";
15
+ import { easeSpring, easeSpringTight } from "./easing.js";
16
+ export { easeSpring, easeSpringTight };
17
+ export const durations = grytDurations;
18
+ /** `withTiming` on the overshooting spring, at the standard duration. */
19
+ export function springy(to, config) {
20
+ "worklet";
21
+ return withTiming(to, { duration: durations.spring, easing: easeSpring, ...config });
22
+ }
23
+ /** `withTiming` on the critically damped spring — travel that must stay in bounds. */
24
+ export function travel(to, config) {
25
+ "worklet";
26
+ return withTiming(to, { duration: durations.spring, easing: easeSpringTight, ...config });
27
+ }
28
+ /** A colour or opacity change, which should not feel sprung. */
29
+ export function fade(to, config) {
30
+ "worklet";
31
+ return withTiming(to, { duration: durations.fast, easing: Easing.out(Easing.quad), ...config });
32
+ }
@@ -0,0 +1,15 @@
1
+ import { useAnimatedStyle } from "react-native-reanimated";
2
+ export interface PressScale {
3
+ onPressIn: () => void;
4
+ onPressOut: () => void;
5
+ style: ReturnType<typeof useAnimatedStyle>;
6
+ }
7
+ /**
8
+ * Scale to `pressed` while held, spring back to 1 on release.
9
+ *
10
+ * Returns handlers to spread onto a Pressable and an animated style for the
11
+ * view that should scale. Both directions use the overshooting curve, so the
12
+ * release settles with the same texture the web has.
13
+ */
14
+ export declare function usePressScale(pressed: number, disabled?: boolean): PressScale;
15
+ //# sourceMappingURL=usePressScale.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePressScale.d.ts","sourceRoot":"","sources":["../../src/motion/usePressScale.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,gBAAgB,EAAkB,MAAM,yBAAyB,CAAC;AAG3E,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,KAAK,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;CAC5C;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,UAAU,CAuB7E"}
@@ -0,0 +1,39 @@
1
+ /* The press half of the web's hover/active pair.
2
+ *
3
+ * `hover:scale-[1.03]` has no meaning on a touch screen and is deliberately
4
+ * not emulated — there is no state between "not touching" and "touching", so
5
+ * inventing one would be a difference from the web rather than a match to it.
6
+ * The `active:` half ports exactly.
7
+ */
8
+ import { useCallback } from "react";
9
+ import { useAnimatedStyle, useSharedValue } from "react-native-reanimated";
10
+ import { springy } from "./motion.js";
11
+ /**
12
+ * Scale to `pressed` while held, spring back to 1 on release.
13
+ *
14
+ * Returns handlers to spread onto a Pressable and an animated style for the
15
+ * view that should scale. Both directions use the overshooting curve, so the
16
+ * release settles with the same texture the web has.
17
+ */
18
+ export function usePressScale(pressed, disabled) {
19
+ const scale = useSharedValue(1);
20
+ // A Reanimated shared value is mutated by design — assigning `.value` is how
21
+ // an animation is started, and the object identity never changes. The
22
+ // immutability rule reads it as a mutated binding, which is what the rule is
23
+ // for everywhere else and a false positive here. Scoped to these two
24
+ // assignments rather than the file.
25
+ /* eslint-disable react-hooks/immutability */
26
+ const onPressIn = useCallback(() => {
27
+ if (disabled)
28
+ return;
29
+ scale.value = springy(pressed);
30
+ }, [disabled, pressed, scale]);
31
+ const onPressOut = useCallback(() => {
32
+ if (disabled)
33
+ return;
34
+ scale.value = springy(1);
35
+ }, [disabled, scale]);
36
+ /* eslint-enable react-hooks/immutability */
37
+ const style = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }] }));
38
+ return { onPressIn, onPressOut, style };
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gryt/ui-native",
3
- "version": "0.2.0",
3
+ "version": "0.3.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.",
@@ -32,15 +32,18 @@
32
32
  },
33
33
  "peerDependencies": {
34
34
  "react": ">=18.3.0 || >=19.0.0",
35
- "react-native": ">=0.76.0"
35
+ "react-native": ">=0.76.0",
36
+ "react-native-reanimated": ">=4.0.0"
36
37
  },
37
38
  "dependencies": {
38
- "@gryt/theme": "^0.1.0"
39
+ "@gryt/theme": "^0.2.0"
39
40
  },
40
41
  "devDependencies": {
41
42
  "@types/react": "^19.0.0",
42
43
  "react": "^19.0.0",
43
44
  "react-native": "^0.76.0",
44
- "vitest": "^3.0.0"
45
- }
45
+ "vitest": "^3.0.0",
46
+ "react-native-reanimated": "^4.5.1"
47
+ },
48
+ "peerDependenciesMeta": {}
46
49
  }