@astryxdesign/core 0.7.9-canary.a1a6151 → 0.7.9-canary.cfd25ce

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 (31) hide show
  1. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  2. package/dist/CheckboxInput/CheckboxInput.js +54 -11
  3. package/dist/InputGroup/InputGroup.d.ts +4 -3
  4. package/dist/InputGroup/InputGroup.d.ts.map +1 -1
  5. package/dist/InputGroup/InputGroup.js +2 -2
  6. package/dist/InputGroup/groupStyles.d.ts +1 -1
  7. package/dist/InputGroup/groupStyles.d.ts.map +1 -1
  8. package/dist/InputGroup/groupStyles.js +1 -1
  9. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  10. package/dist/RadioList/RadioListItem.js +18 -3
  11. package/dist/astryx.css +5 -1
  12. package/dist/gestures/PanGestureRecognizer.d.ts +11 -1
  13. package/dist/gestures/PanGestureRecognizer.d.ts.map +1 -1
  14. package/dist/gestures/PanGestureRecognizer.js +15 -1
  15. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts +6 -1
  16. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts.map +1 -1
  17. package/dist/utils/touchHitArea.stylex.d.ts +35 -0
  18. package/dist/utils/touchHitArea.stylex.d.ts.map +1 -0
  19. package/dist/utils/touchHitArea.stylex.js +32 -0
  20. package/package.json +4 -4
  21. package/src/CheckboxInput/CheckboxInput.doc.mjs +3 -2
  22. package/src/CheckboxInput/CheckboxInput.tsx +71 -9
  23. package/src/InputGroup/InputGroup.doc.mjs +4 -3
  24. package/src/InputGroup/InputGroup.tsx +4 -3
  25. package/src/InputGroup/groupStyles.ts +3 -1
  26. package/src/RadioList/RadioListItem.doc.mjs +3 -2
  27. package/src/RadioList/RadioListItem.tsx +21 -2
  28. package/src/gestures/PanGestureRecognizer.ts +19 -2
  29. package/src/gestures/README.md +2 -1
  30. package/src/gestures/ScreenEdgePanGestureRecognizer.ts +6 -1
  31. package/src/utils/touchHitArea.stylex.ts +41 -0
@@ -1 +1 @@
1
- {"version":3,"file":"CheckboxInput.d.ts","sourceRoot":"","sources":["../../src/CheckboxInput/CheckboxInput.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"CheckboxInput.d.ts","sourceRoot":"","sources":["../../src/CheckboxInput/CheckboxInput.tsx"],"names":[],"mappings":"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"}
@@ -36,6 +36,8 @@ import { usePressFeedback } from "../hooks/usePressFeedback.js";
36
36
  import { useIndicator } from "../Indicator/useIndicator.js";
37
37
  import { themeProps } from "../utils/themeProps.js";
38
38
  import { useMergedRefs } from "../hooks/useMergedRefs.js";
39
+ import { watchHitArea, writeHitInsets } from "../utils/hitArea.js";
40
+ import { touchHitAreaStyles } from "../utils/touchHitArea.stylex.js";
39
41
  import { registerEnglishFallback, useComponentTranslator } from "../i18n/useComponentTranslator.js";
40
42
  import english from "../i18n/generated-english/CheckboxInput/CheckboxInput.generated.js";
41
43
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
@@ -62,6 +64,10 @@ const styles = {
62
64
  kOIVth: "xxhr3t",
63
65
  $$css: true
64
66
  },
67
+ hitAreaHost: {
68
+ kVAEAm: "xrxqdxo",
69
+ $$css: true
70
+ },
65
71
  indicatorPressImage: {
66
72
  "--_indicator-press-image": "x1dueop1 x189o2zt",
67
73
  $$css: true
@@ -210,6 +216,49 @@ export function CheckboxInput({
210
216
  const {
211
217
  focusProps
212
218
  } = useIndicatorFocusRing(indicatorRef);
219
+
220
+ // Under a coarse pointer a labelled checkbox's row grows toward 44 px
221
+ // through an invisible band, and nothing changes size. A label-hidden
222
+ // checkbox is a marker in another component's row, which keeps its target.
223
+ const hasHitArea = !isLabelHidden && !isDisabled && !isReadOnly;
224
+ const inputRef = useRef(null);
225
+ // The watch lives as long as the row is attached: the ref's cleanup stops
226
+ // it, and a replaced row starts its own.
227
+ const watchRowHitArea = useCallback(row => {
228
+ if (row == null || !hasHitArea) {
229
+ return;
230
+ }
231
+ return watchHitArea(row, insets => writeHitInsets(row, insets));
232
+ }, [hasHitArea]);
233
+ // The disabled-message tooltip listens for hover and focus on the whole
234
+ // row, a larger trigger. It anchors on the checkbox itself (below), so it
235
+ // shows next to the control. Its handlers check isEnabled, so the row
236
+ // always attaches them.
237
+ const rowRef = useMergedRefs(disabledMessageTooltip.interactionRef, watchRowHitArea);
238
+ // Where a press on the row itself began. A browser can move a tap's click
239
+ // onto the row's edge, so the band is judged from the press.
240
+ const pressPointRef = useRef(null);
241
+ const handleRowPointerDown = event => {
242
+ pressPointRef.current = event.target === event.currentTarget ? {
243
+ x: event.clientX,
244
+ y: event.clientY
245
+ } : null;
246
+ };
247
+ // A press that began on the band, outside the row's box, toggles the
248
+ // checkbox, as a tap on the box does. A click from code has no press count
249
+ // (`detail` is 0), so it never toggles from here.
250
+ const handleRowClick = event => {
251
+ const press = pressPointRef.current;
252
+ pressPointRef.current = null;
253
+ if (press == null || event.detail === 0 || event.target !== event.currentTarget) {
254
+ return;
255
+ }
256
+ const box = event.currentTarget.getBoundingClientRect();
257
+ const isOnBand = press.x < box.left || press.x > box.right || press.y < box.top || press.y > box.bottom;
258
+ if (isOnBand) {
259
+ inputRef.current?.click();
260
+ }
261
+ };
213
262
  const isIndeterminate = optimisticValue === 'indeterminate';
214
263
  const isChecked = optimisticValue === true;
215
264
 
@@ -249,17 +298,11 @@ export function CheckboxInput({
249
298
  size
250
299
  }), stylex.props(width != null && dynamicWidthStyles.width(width), xstyle), className, style),
251
300
  children: [/*#__PURE__*/_jsxs("div", {
252
- ref: el => {
253
- // Interaction (hover/focus) listeners for the disabled-message
254
- // tooltip attach to the whole row for a larger trigger target;
255
- // positioning anchors on the checkbox itself (below) so the tooltip
256
- // appears next to the control, not the far edge of the row.
257
- // Handlers are gated internally by isEnabled, so attaching
258
- // unconditionally is safe.
259
- disabledMessageTooltip.interactionRef(el);
260
- },
301
+ ref: rowRef,
302
+ onPointerDown: hasHitArea ? handleRowPointerDown : undefined,
303
+ onClick: hasHitArea ? handleRowClick : undefined,
261
304
  ...(isDisabled ? undefined : pressable),
262
- ...stylex.props(styles.container, isLabelHidden && styles.containerLabelHidden,
305
+ ...stylex.props(styles.container, isLabelHidden && styles.containerLabelHidden, hasHitArea && styles.hitAreaHost, hasHitArea && touchHitAreaStyles.layer,
263
306
  // Hover and focus reach the checkbox visual through this ancestor
264
307
  // marker rather than props, so the whole row drives it.
265
308
  !isDisabled && indicatorScope, !isDisabled && styles.indicatorPressImage,
@@ -272,7 +315,7 @@ export function CheckboxInput({
272
315
  ...focusProps,
273
316
  children: [/*#__PURE__*/_jsx("input", {
274
317
  ...rest,
275
- ref: useMergedRefs(ref, indeterminateRef, disabledMessageTooltip.positionRef),
318
+ ref: useMergedRefs(ref, indeterminateRef, disabledMessageTooltip.positionRef, inputRef),
276
319
  id: id,
277
320
  type: "checkbox"
278
321
  // Withhold the name while disabled: the input stays focusable (not
@@ -59,7 +59,8 @@ export interface InputGroupProps extends Omit<BaseProps<HTMLDivElement>, 'childr
59
59
  */
60
60
  size?: InputGroupSize;
61
61
  /**
62
- * Status indicator applied to the group border.
62
+ * Validation status. Its message shows below the group. The group border
63
+ * does not change; a theme can style the row from `data-status`.
63
64
  */
64
65
  status?: InputStatus;
65
66
  /**
@@ -72,8 +73,8 @@ export interface InputGroupProps extends Omit<BaseProps<HTMLDivElement>, 'childr
72
73
  'data-testid'?: string;
73
74
  }
74
75
  /**
75
- * Groups an input with prefix/suffix addons in a visually connected
76
- * container with shared border and focus ring.
76
+ * Groups an input with prefix and suffix addons in one connected row. A
77
+ * focused member stays above its neighbours, so its focus ring shows in full.
77
78
  *
78
79
  * @example
79
80
  * ```
@@ -1 +1 @@
1
- {"version":3,"file":"InputGroup.d.ts","sourceRoot":"","sources":["../../src/InputGroup/InputGroup.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAsB,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAG1D,OAAO,EAAQ,KAAK,WAAW,EAAC,MAAM,UAAU,CAAC;AAGjD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAK5C,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AA0BhD,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAC3C,SAAS,CAAC,cAAc,CAAC,EACzB,UAAU,CACX;IACC,mDAAmD;IACnD,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAEhC;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;IAEpB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,IAAI,CAAC,EAAE,cAAc,CAAC;IAEtB;;OAEG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IAErB;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,EACzB,QAAQ,EACR,KAAK,EACL,aAAqB,EACrB,WAAW,EACX,UAAU,EAAE,cAAsB,EAClC,UAAkB,EAClB,UAAkB,EAClB,IAAI,EAAE,QAAQ,EACd,MAAM,EACN,YAAY,EACZ,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,aAAa,EAAE,MAAM,EACrB,GAAG,IAAI,EACR,EAAE,eAAe,+BA8EjB;yBA/Fe,UAAU"}
1
+ {"version":3,"file":"InputGroup.d.ts","sourceRoot":"","sources":["../../src/InputGroup/InputGroup.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAsB,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAG1D,OAAO,EAAQ,KAAK,WAAW,EAAC,MAAM,UAAU,CAAC;AAGjD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAK5C,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AA0BhD,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAC3C,SAAS,CAAC,cAAc,CAAC,EACzB,UAAU,CACX;IACC,mDAAmD;IACnD,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAEhC;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;IAEpB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,IAAI,CAAC,EAAE,cAAc,CAAC;IAEtB;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IAErB;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,EACzB,QAAQ,EACR,KAAK,EACL,aAAqB,EACrB,WAAW,EACX,UAAU,EAAE,cAAsB,EAClC,UAAkB,EAClB,UAAkB,EAClB,IAAI,EAAE,QAAQ,EACd,MAAM,EACN,YAAY,EACZ,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,aAAa,EAAE,MAAM,EACrB,GAAG,IAAI,EACR,EAAE,eAAe,+BA8EjB;yBA/Fe,UAAU"}
@@ -57,8 +57,8 @@ const sizeStyles = {
57
57
  }
58
58
  };
59
59
  /**
60
- * Groups an input with prefix/suffix addons in a visually connected
61
- * container with shared border and focus ring.
60
+ * Groups an input with prefix and suffix addons in one connected row. A
61
+ * focused member stays above its neighbours, so its focus ring shows in full.
62
62
  *
63
63
  * @example
64
64
  * ```
@@ -33,7 +33,7 @@ export declare const groupStyles: Readonly<{
33
33
  readonly borderStartEndRadius: stylex.StyleXClassNameFor<"borderStartEndRadius", 0 | "8px">;
34
34
  readonly borderEndEndRadius: stylex.StyleXClassNameFor<"borderEndEndRadius", 0 | "8px">;
35
35
  readonly ':focus-within': stylex.StyleXClassNameFor<":focus-within", {
36
- readonly zIndex: 1;
36
+ readonly zIndex: 2;
37
37
  }>;
38
38
  }>;
39
39
  }>;
@@ -1 +1 @@
1
- {"version":3,"file":"groupStyles.d.ts","sourceRoot":"","sources":["../../src/InputGroup/groupStyles.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,OAAO,KAAK,MAAM,MAAM,kBAAkB,CAAC;AAG3C;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB;;CAEtB,CAAC;AA0BX,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;EAwDtB,CAAC"}
1
+ {"version":3,"file":"groupStyles.d.ts","sourceRoot":"","sources":["../../src/InputGroup/groupStyles.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,OAAO,KAAK,MAAM,MAAM,kBAAkB,CAAC;AAG3C;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB;;CAEtB,CAAC;AA0BX,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;EA0DtB,CAAC"}
@@ -62,7 +62,7 @@ export const groupStyles = {
62
62
  kVL7Gh: "xbiv7yw x1xrp5p4",
63
63
  kfmiAY: "x1ga7v0g x1ursdma",
64
64
  kT0f0o: "x16uus16 x1qa286i",
65
- kXnDq0: "x1rm9qnc",
65
+ kXnDq0: "x1pc0aet",
66
66
  $$css: true
67
67
  }
68
68
  };
@@ -1 +1 @@
1
- {"version":3,"file":"RadioListItem.d.ts","sourceRoot":"","sources":["../../src/RadioList/RadioListItem.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,EAAqB,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAGhE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAY5C,OAAO,EAAO,KAAK,SAAS,EAAC,MAAM,cAAc,CAAC;AAsIlD,MAAM,WAAW,kBAAmB,SAAQ,SAAS,CAAC,cAAc,CAAC;IACnE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;;;;OAKG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB;;;;;;;;;;OAUG;IACH,KAAK,EAAE,SAAS,CAAC;IACjB;;;;;;;;;;;;;;OAcG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;OAEG;IACH,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB;;OAEG;IACH,UAAU,CAAC,EAAE,SAAS,CAAC;CACxB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,EAC5B,GAAG,EACH,KAAK,EACL,YAAY,EAAE,SAAS,EACvB,KAAK,EACL,WAAW,EACX,UAAU,EAAE,cAAsB,EAClC,YAAY,EACZ,UAAU,EACV,KAAK,EACL,MAAM,EACN,SAAS,EACT,KAAK,EACL,OAAO,EACP,GAAG,IAAI,EACR,EAAE,kBAAkB,qBAuLpB;yBAtMe,aAAa"}
1
+ {"version":3,"file":"RadioListItem.d.ts","sourceRoot":"","sources":["../../src/RadioList/RadioListItem.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,EAAkC,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAG7E,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAe5C,OAAO,EAAO,KAAK,SAAS,EAAC,MAAM,cAAc,CAAC;AAsIlD,MAAM,WAAW,kBAAmB,SAAQ,SAAS,CAAC,cAAc,CAAC;IACnE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;;;;OAKG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB;;;;;;;;;;OAUG;IACH,KAAK,EAAE,SAAS,CAAC;IACjB;;;;;;;;;;;;;;OAcG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;OAEG;IACH,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB;;OAEG;IACH,UAAU,CAAC,EAAE,SAAS,CAAC;CACxB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,EAC5B,GAAG,EACH,KAAK,EACL,YAAY,EAAE,SAAS,EACvB,KAAK,EACL,WAAW,EACX,UAAU,EAAE,cAAsB,EAClC,YAAY,EACZ,UAAU,EACV,KAAK,EACL,MAAM,EACN,SAAS,EACT,KAAK,EACL,OAAO,EACP,GAAG,IAAI,EACR,EAAE,kBAAkB,qBAuMpB;yBAtNe,aAAa"}
@@ -17,7 +17,7 @@
17
17
  * - /apps/storybook/stories/RadioList.stories.tsx
18
18
  * - /packages/cli/assets/templates/blocks/components/RadioList/ (showcase blocks)
19
19
  */
20
- import React, { use, useId, useRef } from 'react';
20
+ import React, { use, useCallback, useId, useRef } from 'react';
21
21
  import * as stylex from '@stylexjs/stylex';
22
22
  import { RadioListContext } from "./RadioList.js";
23
23
  import { RadioListReasonContext } from "./RadioListReasonContext.js";
@@ -31,6 +31,9 @@ import "../Indicator/indicator.markers.stylex.js";
31
31
  import { indicatorScope } from "../Indicator/indicator.markers.stylex.js";
32
32
  import { useIndicatorFocusRing } from "../hooks/useIndicatorFocusRing.js";
33
33
  import { usePressFeedback } from "../hooks/usePressFeedback.js";
34
+ import { useMergedRefs } from "../hooks/useMergedRefs.js";
35
+ import { watchHitArea, writeHitInsets } from "../utils/hitArea.js";
36
+ import { touchHitAreaStyles } from "../utils/touchHitArea.stylex.js";
34
37
  import { useIndicator } from "../Indicator/useIndicator.js";
35
38
  import { Item } from "../Item/Item.js";
36
39
  import { themeProps } from "../utils/themeProps.js";
@@ -186,6 +189,18 @@ export function RadioListItem({
186
189
  // since the visible label is now a plain (non-`<label>`) text node — a real
187
190
  // `<label htmlFor>` would double-fire under delegation.
188
191
  const radioRef = useRef(null);
192
+
193
+ // Under a coarse pointer an option's row grows toward 44 px through an
194
+ // invisible band, and the row keeps its size. A tap on the band lands on
195
+ // the row, which hands it to the radio. The watch lives as long as the row
196
+ // is attached: the ref's cleanup stops it.
197
+ const watchRowHitArea = useCallback(row => {
198
+ if (row == null || isDisabled) {
199
+ return;
200
+ }
201
+ return watchHitArea(row, insets => writeHitInsets(row, insets));
202
+ }, [isDisabled]);
203
+ const mergedRowRef = useMergedRefs(ref, watchRowHitArea);
189
204
  const radioCircle = /*#__PURE__*/_jsxs("div", {
190
205
  ...stylex.props(styles.radioWrapper, wrapperSizeStyles[size], !isDisabled && styles.indicatorPressOverlay),
191
206
  ...focusProps,
@@ -251,7 +266,7 @@ export function RadioListItem({
251
266
  children: [radioCircle, startContent]
252
267
  }) : radioCircle;
253
268
  return /*#__PURE__*/_jsx(Item, {
254
- ref: ref,
269
+ ref: mergedRowRef,
255
270
  ...(isDisabled ? undefined : pressable),
256
271
  startContent: mediaContent
257
272
  // Delegate row-surface clicks (label text, description, and the empty
@@ -283,7 +298,7 @@ export function RadioListItem({
283
298
  // interactive hover/press background. Applied after Item's own base
284
299
  // styles so it wins within the base layer; a `radio-list-item` theme
285
300
  // still overrides it from the higher `astryx-theme` layer.
286
- rowStyles.root, !isDisabled && rowStyles.press, xstyle],
301
+ rowStyles.root, !isDisabled && rowStyles.press, !isDisabled && touchHitAreaStyles.layer, xstyle],
287
302
  ...mergeProps(
288
303
  // One target for every row, carrying its size and runtime state so a
289
304
  // theme can express "selected option at large" or restyle disabled
package/dist/astryx.css CHANGED
@@ -1633,7 +1633,7 @@
1633
1633
  .xttggg:is([dir="rtl"] *):not(#\#):not(#\#):not(#\#){transform:translateX(100%)}
1634
1634
  .x1nyx83j:is([dir="rtl"] *):not(#\#):not(#\#):not(#\#){transform:translateX(50%)}
1635
1635
  .x9cjr8z:popover-open:not(#\#):not(#\#):not(#\#){transform:translateY(0)}
1636
- .x1rm9qnc:focus-within:not(#\#):not(#\#):not(#\#){z-index:1}
1636
+ .x1pc0aet:focus-within:not(#\#):not(#\#):not(#\#){z-index:2}
1637
1637
  .xf5goe6.xf5goe6:where(.astryx-input-group > [data-astryx-input-group-item]):focus-visible:not(#\#):not(#\#):not(#\#){z-index:2}
1638
1638
  .x151j4nw:has(input[aria-disabled="true"]:focus-visible):not(#\#):not(#\#):not(#\#){box-shadow:inset 0 0 0 2px var(--color-accent-muted)}
1639
1639
  .xwmge8v:has(> [aria-disabled="true"]:focus-visible):not(#\#):not(#\#):not(#\#){box-shadow:inset 0 0 0 2px var(--color-accent-muted)}
@@ -2322,6 +2322,7 @@
2322
2322
  @media (prefers-reduced-motion: reduce) and (not (pointer: coarse)) and (not (pointer: coarse)){.xxsp3er.xxsp3er[data-astryx-press="fading"]:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#){animation-name:none}}
2323
2323
  @media (forced-colors: active){.x1pjcgc3.x1pjcgc3[data-astryx-press="fading"]:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#){background-color:Highlight}}
2324
2324
  @media (forced-colors: active){.x1r4mieh.x1r4mieh[data-astryx-press="on"]:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#){background-color:Highlight}}
2325
+ @media (pointer: coarse){.x16x82qr.x16x82qr:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::after{inset:calc(var(--astryx-hit-inset-top,0px) * -1) calc(var(--astryx-hit-inset-right,0px) * -1) calc(var(--astryx-hit-inset-bottom,0px) * -1) calc(var(--astryx-hit-inset-left,0px) * -1)}}
2325
2326
  @starting-style{@media (prefers-reduced-motion: reduce) and (pointer: coarse){.xgo67oj.xgo67oj.xgo67oj:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#){opacity:0}}}
2326
2327
  @starting-style{@media (prefers-reduced-motion: reduce){.xspp5ug.xspp5ug.xspp5ug:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#){transform:translateY(0)}}}
2327
2328
  @media (pointer: coarse){.x10ogxw0.x10ogxw0.x10ogxw0:where(:not(:disabled,[aria-disabled="true"]))[data-astryx-press="on"]:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#){background-image:none}}
@@ -2493,7 +2494,10 @@
2493
2494
  .x1c108yo:is([dir="rtl"] *):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::after{transform:translateX(-100%)}
2494
2495
  .x12ayhd5:is([dir="rtl"] *):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::after{transform:translateX(100%)}
2495
2496
  @media (forced-colors: none){.x1my3e5g.x1my3e5g:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::before{background-color:var(--color-overlay-pressed)}}
2497
+ @media (pointer: coarse){.x1c440c0.x1c440c0:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::after{clip-path:polygon(evenodd,0 0,100% 0,100% 100%,0 100%,0 0,var(--astryx-hit-inset-left,0px) var(--astryx-hit-inset-top,0px),calc(100% - var(--astryx-hit-inset-right,0px)) var(--astryx-hit-inset-top,0px),calc(100% - var(--astryx-hit-inset-right,0px)) calc(100% - var(--astryx-hit-inset-bottom,0px)),var(--astryx-hit-inset-left,0px) calc(100% - var(--astryx-hit-inset-bottom,0px)),var(--astryx-hit-inset-left,0px) var(--astryx-hit-inset-top,0px))}}
2498
+ @media (pointer: coarse){.x1fjtqre.x1fjtqre:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::after{content:""}}
2496
2499
  @media (pointer: coarse){.x1ep27e9.x1ep27e9:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::after{display:block}}
2500
+ @media (pointer: coarse){.x7b72wb.x7b72wb:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::after{position:absolute}}
2497
2501
  @media (prefers-reduced-motion: reduce){.xhhm9v0.xhhm9v0:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::before{transition-delay:0s}}
2498
2502
  @media (prefers-reduced-motion: reduce){.x1viac0w.x1viac0w:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::backdrop{transition-duration:.01s}}
2499
2503
  @media (prefers-reduced-motion: reduce){.x13sdc92.x13sdc92:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::before{transition-duration:0s}}
@@ -6,7 +6,12 @@ export interface PanGestureRecognizerOptions extends GestureRecognizerOptions<Pa
6
6
  minimumNumberOfTouches?: number;
7
7
  /** @default Infinity */
8
8
  maximumNumberOfTouches?: number;
9
- hysteresis?: number;
9
+ /**
10
+ * The travel that begins the pan, px. A function gives each press its own
11
+ * threshold: it reads the press's first touch when that touch lands.
12
+ * @default UIKit's 10
13
+ */
14
+ hysteresis?: number | ((touch: GestureTouch) => number);
10
15
  /** Movement that may begin the pan; anything else fails it. @default 'any' */
11
16
  direction?: PanDirection;
12
17
  /**
@@ -51,6 +56,11 @@ export declare class PanGestureRecognizer extends GestureRecognizer {
51
56
  /** The movement the begin threshold took (excluded from `translation()`). */
52
57
  travelAtBegan: Point;
53
58
  private readonly tracker;
59
+ /**
60
+ * Read once per press. A finger that lands later keeps the press's
61
+ * threshold.
62
+ */
63
+ private readonly hysteresisFor;
54
64
  /** The lift's event time once the pan ended: its release (AST-100 FR2). */
55
65
  private releaseTime;
56
66
  /** The centroid where the gesture started, in client px. */
@@ -1 +1 @@
1
- {"version":3,"file":"PanGestureRecognizer.d.ts","sourceRoot":"","sources":["../../src/gestures/PanGestureRecognizer.ts"],"names":[],"mappings":"AAuCA,OAAO,EAEL,iBAAiB,EAEjB,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,EACzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAC,mBAAmB,EAAE,YAAY,EAAE,KAAK,EAAC,MAAM,SAAS,CAAC;AAGtE,MAAM,MAAM,YAAY,GACtB,KAAK,GAAG,YAAY,GAAG,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,MAAM,CAAC;AAEvE,MAAM,WAAW,2BAA4B,SAAQ,wBAAwB,CAAC,oBAAoB,CAAC;IACjG,iBAAiB;IACjB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,wBAAwB;IACxB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,YAAY,CAAC;IACzB;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IACjD,4EAA4E;IAC5E,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAWD,qBAAa,oBAAqB,SAAQ,iBAAiB;IACzD,IAAa,IAAI,IAAI,iBAAiB,CAAC,MAAM,CAAC,CAE7C;IAED,2FAA2F;IAC3F,SAAS,KAAK,MAAM,IAAI,KAAK,CAE5B;IAED,sBAAsB,EAAE,MAAM,CAAC;IAC/B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,YAAY,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,OAAO,CAAC;IACpB,yDAAyD;IACzD,UAAU,EAAE,GAAG,GAAG,GAAG,GAAG,IAAI,CAAQ;IACpC,6EAA6E;IAC7E,aAAa,EAAE,KAAK,CAAQ;IAE5B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkB;IAC1C,2EAA2E;IAC3E,OAAO,CAAC,WAAW,CAAiC;IACpD,4DAA4D;IAC5D,OAAO,CAAC,MAAM,CAAe;IAC7B,6EAA6E;IAC7E,OAAO,CAAC,MAAM,CAAe;IAC7B,qEAAqE;IACrE,OAAO,CAAC,GAAG,CAAe;IAC1B,OAAO,CAAC,MAAM,CAAe;gBAEjB,OAAO,GAAE,2BAAgC;IAarD;;;OAGG;IACH,WAAW,IAAI,KAAK;IAOpB;;;;;OAKG;IACH,YAAY,CAAC,SAAS,EAAE,YAAY,GAAG,IAAI;IAI3C,kFAAkF;IAClF,cAAc,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAIlC,wEAAwE;IACxE,gBAAgB,IAAI,KAAK;IAIzB;;;;OAIG;IACH,QAAQ,IAAI,KAAK;IAIjB;;;;OAIG;IACH,cAAc,IAAI,KAAK;IAQvB;;;;;;;OAOG;IACH,IAAa,mBAAmB,IAAI,mBAAmB,CAkBtD;cAEkB,YAAY,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO;cAW1C,YAAY,CAC7B,OAAO,EAAE,aAAa,CAAC,YAAY,CAAC,EACpC,KAAK,EAAE,mBAAmB,GACzB,IAAI;cAmBY,YAAY,CAC7B,QAAQ,EAAE,aAAa,CAAC,YAAY,CAAC,EACrC,KAAK,EAAE,mBAAmB,GACzB,IAAI;cASY,YAAY,CAC7B,OAAO,EAAE,aAAa,CAAC,YAAY,CAAC,EACpC,KAAK,EAAE,mBAAmB,GACzB,IAAI;cAoBY,KAAK,IAAI,IAAI;IAWhC;;;;OAIG;IACH,SAAS,CAAC,QAAQ,CAAC,WAAW,EAAE,KAAK,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM;IAoBjE,OAAO,CAAC,QAAQ;IAsChB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;IAUrB;;;;;;;OAOG;cACgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAsBvD,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,KAAK;IAWb,kEAAkE;IAClE,OAAO,CAAC,MAAM;IAad,OAAO,CAAC,MAAM;CASf"}
1
+ {"version":3,"file":"PanGestureRecognizer.d.ts","sourceRoot":"","sources":["../../src/gestures/PanGestureRecognizer.ts"],"names":[],"mappings":"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"}
@@ -74,6 +74,12 @@ export class PanGestureRecognizer extends GestureRecognizer {
74
74
  lockedAxis = null;
75
75
  /** The movement the begin threshold took (excluded from `translation()`). */
76
76
  travelAtBegan = ZERO;
77
+
78
+ /**
79
+ * Read once per press. A finger that lands later keeps the press's
80
+ * threshold.
81
+ */
82
+
77
83
  /** The lift's event time once the pan ended: its release (AST-100 FR2). */
78
84
  releaseTime = undefined;
79
85
  /** The centroid where the gesture started, in client px. */
@@ -87,7 +93,11 @@ export class PanGestureRecognizer extends GestureRecognizer {
87
93
  super(options);
88
94
  this.minimumNumberOfTouches = options.minimumNumberOfTouches ?? 1;
89
95
  this.maximumNumberOfTouches = options.maximumNumberOfTouches ?? Number.POSITIVE_INFINITY;
90
- this.hysteresis = options.hysteresis ?? UIKIT_PAN.hysteresis.value;
96
+ const {
97
+ hysteresis
98
+ } = options;
99
+ this.hysteresisFor = typeof hysteresis === 'function' ? hysteresis : null;
100
+ this.hysteresis = typeof hysteresis === 'number' ? hysteresis : UIKIT_PAN.hysteresis.value;
91
101
  this.direction = options.direction ?? 'any';
92
102
  this.axisRatio = options.axisRatio ?? 1;
93
103
  this.lockAxis = options.lockAxis ?? false;
@@ -177,6 +187,10 @@ export class PanGestureRecognizer extends GestureRecognizer {
177
187
  }
178
188
  touchesBegan(touches, event) {
179
189
  if (this.numberOfTouches === touches.length) {
190
+ const first = touches[0];
191
+ if (this.hysteresisFor != null && first != null) {
192
+ this.hysteresis = this.hysteresisFor(first);
193
+ }
180
194
  this.origin = centroid(this.touches.values());
181
195
  this.adjust = ZERO;
182
196
  this.raw = ZERO;
@@ -12,11 +12,16 @@ type PhysicalEdge = 'left' | 'right';
12
12
  * A screen-edge pan is a finger's: it takes no `allowedPointerTypes` and no
13
13
  * `allowedScrollTypes`, so neither a mouse, a trackpad nor a pen drives it.
14
14
  */
15
- export interface ScreenEdgePanGestureRecognizerOptions extends Omit<PanGestureRecognizerOptions, 'direction' | 'allowedPointerTypes' | 'allowedScrollTypes'> {
15
+ export interface ScreenEdgePanGestureRecognizerOptions extends Omit<PanGestureRecognizerOptions, 'direction' | 'allowedPointerTypes' | 'allowedScrollTypes' | 'hysteresis'> {
16
16
  /** The edges a touch may start from. @default ['leading'] */
17
17
  edges?: ReadonlyArray<ScreenEdge>;
18
18
  /** Width of the band at each edge, px. */
19
19
  edgeRegion?: number;
20
+ /**
21
+ * The travel that begins the pan in its edge zone, px. It is one number for
22
+ * every press, since the zone sets the threshold itself. @default UIKit's 15
23
+ */
24
+ hysteresis?: number;
20
25
  }
21
26
  export declare class ScreenEdgePanGestureRecognizer extends PanGestureRecognizer {
22
27
  get kind(): PanGestureRecognizer['kind'];
@@ -1 +1 @@
1
- {"version":3,"file":"ScreenEdgePanGestureRecognizer.d.ts","sourceRoot":"","sources":["../../src/gestures/ScreenEdgePanGestureRecognizer.ts"],"names":[],"mappings":"AAsCA,OAAO,EAAmB,KAAK,mBAAmB,EAAC,MAAM,qBAAqB,CAAC;AAC/E,OAAO,EACL,oBAAoB,EACpB,KAAK,2BAA2B,EACjC,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAC,mBAAmB,EAAE,YAAY,EAAC,MAAM,SAAS,CAAC;AAS/D;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,UAAU,CAAC;AAEhD,kDAAkD;AAClD,KAAK,YAAY,GAAG,MAAM,GAAG,OAAO,CAAC;AAarC;;;GAGG;AACH,MAAM,WAAW,qCAAsC,SAAQ,IAAI,CACjE,2BAA2B,EAC3B,WAAW,GAAG,qBAAqB,GAAG,oBAAoB,CAC3D;IACC,6DAA6D;IAC7D,KAAK,CAAC,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IAClC,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,8BAA+B,SAAQ,oBAAoB;IACtE,IAAa,IAAI,IAAI,oBAAoB,CAAC,MAAM,CAAC,CAEhD;IAED,KAAK,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,0DAA0D;IAC1D,UAAU,EAAE,YAAY,GAAG,IAAI,CAAQ;IACvC;;;;OAIG;IACH,qBAAqB,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAQ;IAClD,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,iFAAiF;IACjF,OAAO,CAAC,MAAM,CAA2B;gBAE7B,OAAO,GAAE,qCAA0C;IAiB/D;;;;;OAKG;IACH,IAAa,mBAAmB,IAAI,mBAAmB,CAWtD;IAED;;;;OAIG;IACH,IAAI,eAAe,IAAI,OAAO,CAE7B;IAED;;;;OAIG;IACH,aAAa,IAAI,IAAI;cAcF,YAAY,CAC7B,OAAO,EAAE,aAAa,CAAC,YAAY,CAAC,EACpC,KAAK,EAAE,mBAAmB,GACzB,IAAI;IAiCP;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ;cAyBG,KAAK,IAAI,IAAI;IAKhC,OAAO,CAAC,MAAM;CAaf"}
1
+ {"version":3,"file":"ScreenEdgePanGestureRecognizer.d.ts","sourceRoot":"","sources":["../../src/gestures/ScreenEdgePanGestureRecognizer.ts"],"names":[],"mappings":"AAsCA,OAAO,EAAmB,KAAK,mBAAmB,EAAC,MAAM,qBAAqB,CAAC;AAC/E,OAAO,EACL,oBAAoB,EACpB,KAAK,2BAA2B,EACjC,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAC,mBAAmB,EAAE,YAAY,EAAC,MAAM,SAAS,CAAC;AAS/D;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,UAAU,CAAC;AAEhD,kDAAkD;AAClD,KAAK,YAAY,GAAG,MAAM,GAAG,OAAO,CAAC;AAarC;;;GAGG;AACH,MAAM,WAAW,qCAAsC,SAAQ,IAAI,CACjE,2BAA2B,EAC3B,WAAW,GAAG,qBAAqB,GAAG,oBAAoB,GAAG,YAAY,CAC1E;IACC,6DAA6D;IAC7D,KAAK,CAAC,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IAClC,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,8BAA+B,SAAQ,oBAAoB;IACtE,IAAa,IAAI,IAAI,oBAAoB,CAAC,MAAM,CAAC,CAEhD;IAED,KAAK,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,0DAA0D;IAC1D,UAAU,EAAE,YAAY,GAAG,IAAI,CAAQ;IACvC;;;;OAIG;IACH,qBAAqB,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAQ;IAClD,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,iFAAiF;IACjF,OAAO,CAAC,MAAM,CAA2B;gBAE7B,OAAO,GAAE,qCAA0C;IAiB/D;;;;;OAKG;IACH,IAAa,mBAAmB,IAAI,mBAAmB,CAWtD;IAED;;;;OAIG;IACH,IAAI,eAAe,IAAI,OAAO,CAE7B;IAED;;;;OAIG;IACH,aAAa,IAAI,IAAI;cAcF,YAAY,CAC7B,OAAO,EAAE,aAAa,CAAC,YAAY,CAAC,EACpC,KAAK,EAAE,mBAAmB,GACzB,IAAI;IAiCP;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ;cAyBG,KAAK,IAAI,IAAI;IAKhC,OAAO,CAAC,MAAM;CAaf"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @file touchHitArea.stylex.ts
3
+ * @input Uses StyleX and the insets that utils/hitArea.ts writes on a host
4
+ * @output Exports `touchHitAreaStyles.layer`, a host's invisible hit band
5
+ * under a coarse pointer
6
+ * @position Internal styling utility for CheckboxInput and RadioListItem
7
+ *
8
+ * The band is the host's `::after`, grown past each edge by the measured
9
+ * inset. A clip path cuts out the host's box, so its content keeps its taps.
10
+ * A tap on the band lands on the host. SYNC: hitArea.ts HIT_INSET_PROPERTIES.
11
+ */
12
+ import * as stylex from '@stylexjs/stylex';
13
+ export declare const touchHitAreaStyles: Readonly<{
14
+ readonly layer: Readonly<{
15
+ readonly '::after': stylex.StyleXClassNameFor<"::after", {
16
+ readonly content: {
17
+ readonly default: null;
18
+ readonly "@media (pointer: coarse)": "\"\"";
19
+ };
20
+ readonly position: {
21
+ readonly default: null;
22
+ readonly "@media (pointer: coarse)": "absolute";
23
+ };
24
+ readonly inset: {
25
+ readonly default: null;
26
+ readonly "@media (pointer: coarse)": "calc(var(--astryx-hit-inset-top, 0px) * -1) calc(var(--astryx-hit-inset-right, 0px) * -1) calc(var(--astryx-hit-inset-bottom, 0px) * -1) calc(var(--astryx-hit-inset-left, 0px) * -1)";
27
+ };
28
+ readonly clipPath: {
29
+ readonly default: null;
30
+ readonly "@media (pointer: coarse)": "polygon(evenodd, 0 0, 100% 0, 100% 100%, 0 100%, 0 0, var(--astryx-hit-inset-left, 0px) var(--astryx-hit-inset-top, 0px), calc(100% - var(--astryx-hit-inset-right, 0px)) var(--astryx-hit-inset-top, 0px), calc(100% - var(--astryx-hit-inset-right, 0px)) calc(100% - var(--astryx-hit-inset-bottom, 0px)), var(--astryx-hit-inset-left, 0px) calc(100% - var(--astryx-hit-inset-bottom, 0px)), var(--astryx-hit-inset-left, 0px) var(--astryx-hit-inset-top, 0px))";
31
+ };
32
+ }>;
33
+ }>;
34
+ }>;
35
+ //# sourceMappingURL=touchHitArea.stylex.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"touchHitArea.stylex.d.ts","sourceRoot":"","sources":["../../src/utils/touchHitArea.stylex.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,MAAM,MAAM,kBAAkB,CAAC;AAU3C,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;EAgB7B,CAAC"}
@@ -0,0 +1,32 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file touchHitArea.stylex.ts
5
+ * @input Uses StyleX and the insets that utils/hitArea.ts writes on a host
6
+ * @output Exports `touchHitAreaStyles.layer`, a host's invisible hit band
7
+ * under a coarse pointer
8
+ * @position Internal styling utility for CheckboxInput and RadioListItem
9
+ *
10
+ * The band is the host's `::after`, grown past each edge by the measured
11
+ * inset. A clip path cuts out the host's box, so its content keeps its taps.
12
+ * A tap on the band lands on the host. SYNC: hitArea.ts HIT_INSET_PROPERTIES.
13
+ */
14
+
15
+ import * as stylex from '@stylexjs/stylex';
16
+
17
+ // StyleX reads these at build time, so they are spelled out.
18
+ const TOUCH = '@media (pointer: coarse)';
19
+ // The insets are measured in the viewport, so the sides are physical.
20
+ const TOP = 'var(--astryx-hit-inset-top, 0px)';
21
+ const RIGHT = 'var(--astryx-hit-inset-right, 0px)';
22
+ const BOTTOM = 'var(--astryx-hit-inset-bottom, 0px)';
23
+ const LEFT = 'var(--astryx-hit-inset-left, 0px)';
24
+ export const touchHitAreaStyles = {
25
+ layer: {
26
+ k5JduY: "x1fjtqre",
27
+ kwXMNM: "x7b72wb",
28
+ kv0HGH: "x16x82qr",
29
+ kJYu5r: "x1c440c0",
30
+ $$css: true
31
+ }
32
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.7.9-canary.a1a6151",
3
+ "version": "0.7.9-canary.cfd25ce",
4
4
  "displayName": "Astryx Core",
5
5
  "description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
6
6
  "author": "Meta Open Source",
@@ -870,9 +870,9 @@
870
870
  "react-dom": ">=19.0.0"
871
871
  },
872
872
  "devDependencies": {
873
- "@astryxdesign/a11y-spec": "0.7.9-canary.a1a6151",
874
- "@astryxdesign/cli": "0.7.9-canary.a1a6151",
875
- "@astryxdesign/interaction-harness": "0.7.9-canary.a1a6151",
873
+ "@astryxdesign/a11y-spec": "0.7.9-canary.cfd25ce",
874
+ "@astryxdesign/cli": "0.7.9-canary.cfd25ce",
875
+ "@astryxdesign/interaction-harness": "0.7.9-canary.cfd25ce",
876
876
  "@babel/cli": "^7.29.7",
877
877
  "@babel/core": "^7.29.7",
878
878
  "@babel/preset-react": "^7.29.7",
@@ -109,7 +109,7 @@ export const docs = {
109
109
  name: 'size',
110
110
  type: "'sm' | 'md'",
111
111
  description:
112
- 'The size of the checkbox. sm for compact layouts, md for default.',
112
+ 'The size of the checkbox. sm for compact layouts, md for default. Under a coarse pointer a labelled checkbox’s target grows toward 44 px through an invisible hit area. It never shrinks below the row and stops at half the gap to a closer neighbour; nothing changes size.',
113
113
  default: "'md'",
114
114
  },
115
115
  {
@@ -361,7 +361,8 @@ export const docsZh = {
361
361
  {
362
362
  name: 'size',
363
363
  type: "'sm' | 'md'",
364
- description: '复选框尺寸。sm 用于紧凑布局,md 为默认。',
364
+ description:
365
+ '复选框尺寸。sm 用于紧凑布局,md 为默认。在粗指针(触摸)下,带可见标签的复选框的目标通过不可见的点击区域向 44 px 扩展。它不小于所在行,与相邻控件较近时止于间距的一半;尺寸不变。',
365
366
  default: "'md'",
366
367
  },
367
368
  {
@@ -24,6 +24,8 @@ import {
24
24
  useTransition,
25
25
  type ChangeEvent,
26
26
  type FocusEvent,
27
+ type MouseEvent as ReactMouseEvent,
28
+ type PointerEvent as ReactPointerEvent,
27
29
  type ReactNode,
28
30
  } from 'react';
29
31
  import * as stylex from '@stylexjs/stylex';
@@ -55,6 +57,8 @@ import {useIndicator} from '../Indicator/useIndicator';
55
57
  import {themeProps} from '../utils/themeProps';
56
58
 
57
59
  import {useMergedRefs} from '../hooks/useMergedRefs';
60
+ import {watchHitArea, writeHitInsets} from '../utils/hitArea';
61
+ import {touchHitAreaStyles} from '../utils/touchHitArea.stylex';
58
62
  import {
59
63
  registerEnglishFallback,
60
64
  useComponentTranslator,
@@ -83,6 +87,10 @@ const styles = stylex.create({
83
87
  containerLabelHidden: {
84
88
  gap: 0,
85
89
  },
90
+ // Under a coarse pointer the row contains its hit band.
91
+ hitAreaHost: {
92
+ position: {default: null, '@media (pointer: coarse)': 'relative'},
93
+ },
86
94
  // An indicator in another package may not name this row's private press
87
95
  // attribute. So the row hands its touch press image down to an indicator
88
96
  // that paints its own press, but only in its own press states. A pressed
@@ -407,6 +415,63 @@ export function CheckboxInput({
407
415
  useBusyAnnouncement(isBusy, t('@astryx.spinner.loading'), indicatorRef);
408
416
  const {focusProps} = useIndicatorFocusRing(indicatorRef);
409
417
 
418
+ // Under a coarse pointer a labelled checkbox's row grows toward 44 px
419
+ // through an invisible band, and nothing changes size. A label-hidden
420
+ // checkbox is a marker in another component's row, which keeps its target.
421
+ const hasHitArea = !isLabelHidden && !isDisabled && !isReadOnly;
422
+ const inputRef = useRef<HTMLInputElement>(null);
423
+ // The watch lives as long as the row is attached: the ref's cleanup stops
424
+ // it, and a replaced row starts its own.
425
+ const watchRowHitArea = useCallback(
426
+ (row: HTMLDivElement | null) => {
427
+ if (row == null || !hasHitArea) {
428
+ return;
429
+ }
430
+ return watchHitArea(row, insets => writeHitInsets(row, insets));
431
+ },
432
+ [hasHitArea],
433
+ );
434
+ // The disabled-message tooltip listens for hover and focus on the whole
435
+ // row, a larger trigger. It anchors on the checkbox itself (below), so it
436
+ // shows next to the control. Its handlers check isEnabled, so the row
437
+ // always attaches them.
438
+ const rowRef = useMergedRefs(
439
+ disabledMessageTooltip.interactionRef,
440
+ watchRowHitArea,
441
+ );
442
+ // Where a press on the row itself began. A browser can move a tap's click
443
+ // onto the row's edge, so the band is judged from the press.
444
+ const pressPointRef = useRef<{x: number; y: number} | null>(null);
445
+ const handleRowPointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {
446
+ pressPointRef.current =
447
+ event.target === event.currentTarget
448
+ ? {x: event.clientX, y: event.clientY}
449
+ : null;
450
+ };
451
+ // A press that began on the band, outside the row's box, toggles the
452
+ // checkbox, as a tap on the box does. A click from code has no press count
453
+ // (`detail` is 0), so it never toggles from here.
454
+ const handleRowClick = (event: ReactMouseEvent<HTMLDivElement>) => {
455
+ const press = pressPointRef.current;
456
+ pressPointRef.current = null;
457
+ if (
458
+ press == null ||
459
+ event.detail === 0 ||
460
+ event.target !== event.currentTarget
461
+ ) {
462
+ return;
463
+ }
464
+ const box = event.currentTarget.getBoundingClientRect();
465
+ const isOnBand =
466
+ press.x < box.left ||
467
+ press.x > box.right ||
468
+ press.y < box.top ||
469
+ press.y > box.bottom;
470
+ if (isOnBand) {
471
+ inputRef.current?.click();
472
+ }
473
+ };
474
+
410
475
  const isIndeterminate = optimisticValue === 'indeterminate';
411
476
  const isChecked = optimisticValue === true;
412
477
 
@@ -455,19 +520,15 @@ export function CheckboxInput({
455
520
  style,
456
521
  )}>
457
522
  <div
458
- ref={el => {
459
- // Interaction (hover/focus) listeners for the disabled-message
460
- // tooltip attach to the whole row for a larger trigger target;
461
- // positioning anchors on the checkbox itself (below) so the tooltip
462
- // appears next to the control, not the far edge of the row.
463
- // Handlers are gated internally by isEnabled, so attaching
464
- // unconditionally is safe.
465
- disabledMessageTooltip.interactionRef(el);
466
- }}
523
+ ref={rowRef}
524
+ onPointerDown={hasHitArea ? handleRowPointerDown : undefined}
525
+ onClick={hasHitArea ? handleRowClick : undefined}
467
526
  {...(isDisabled ? undefined : pressable)}
468
527
  {...stylex.props(
469
528
  styles.container,
470
529
  isLabelHidden && styles.containerLabelHidden,
530
+ hasHitArea && styles.hitAreaHost,
531
+ hasHitArea && touchHitAreaStyles.layer,
471
532
  // Hover and focus reach the checkbox visual through this ancestor
472
533
  // marker rather than props, so the whole row drives it.
473
534
  !isDisabled && indicatorScope,
@@ -490,6 +551,7 @@ export function CheckboxInput({
490
551
  ref,
491
552
  indeterminateRef,
492
553
  disabledMessageTooltip.positionRef,
554
+ inputRef,
493
555
  )}
494
556
  id={id}
495
557
  type="checkbox"
@@ -24,7 +24,7 @@ export const docs = {
24
24
  ],
25
25
  },
26
26
  description:
27
- 'Groups an input with prefix/suffix addons in a visually connected container with shared border and focus ring.',
27
+ 'Groups an input with prefix and suffix addons in one connected row. A focused member stays above its neighbours, so its focus ring shows in full.',
28
28
  props: [
29
29
  {
30
30
  name: 'children',
@@ -59,7 +59,7 @@ export const docs = {
59
59
  {
60
60
  name: 'isOptional',
61
61
  type: 'boolean',
62
- description: 'Show "(optional)" indicator.',
62
+ description: 'Show the "Optional" indicator after the label.',
63
63
  default: 'false',
64
64
  },
65
65
  {
@@ -77,7 +77,8 @@ export const docs = {
77
77
  {
78
78
  name: 'status',
79
79
  type: "{type: 'warning' | 'error' | 'success', message?: string}",
80
- description: 'Status indicator applied to the group border.',
80
+ description:
81
+ 'Validation status. Its message shows below the group. The group border does not change; a theme can style the row from `data-status`.',
81
82
  },
82
83
  {
83
84
  name: 'labelTooltip',
@@ -109,7 +109,8 @@ export interface InputGroupProps extends Omit<
109
109
  size?: InputGroupSize;
110
110
 
111
111
  /**
112
- * Status indicator applied to the group border.
112
+ * Validation status. Its message shows below the group. The group border
113
+ * does not change; a theme can style the row from `data-status`.
113
114
  */
114
115
  status?: InputStatus;
115
116
 
@@ -125,8 +126,8 @@ export interface InputGroupProps extends Omit<
125
126
  }
126
127
 
127
128
  /**
128
- * Groups an input with prefix/suffix addons in a visually connected
129
- * container with shared border and focus ring.
129
+ * Groups an input with prefix and suffix addons in one connected row. A
130
+ * focused member stays above its neighbours, so its focus ring shows in full.
130
131
  *
131
132
  * @example
132
133
  * ```
@@ -96,8 +96,10 @@ export const groupStyles = stylex.create({
96
96
  default: 0,
97
97
  [IS_LAST_ITEM]: radiusVars['--radius-element'],
98
98
  },
99
+ // Input wrappers already sit at z-index 1. A focused member goes one
100
+ // higher, so an unfocused input beside it cannot cover its focus ring.
99
101
  ':focus-within': {
100
- zIndex: 1,
102
+ zIndex: 2,
101
103
  },
102
104
  },
103
105
  });
@@ -8,7 +8,7 @@ export const docs = {
8
8
  displayName: 'Radio List Item',
9
9
  isHiddenFromOverview: true,
10
10
  description:
11
- 'Individual radio item with label, description, and content slots.',
11
+ 'Individual radio item with label, description, and content slots. Under a coarse pointer an enabled option’s target grows toward 44 px through an invisible hit area. It never shrinks below the row and stops at half the gap to a closer neighbour; the row keeps its size. That hit area is the row’s `::after`, so decorate a row with a child.',
12
12
  // RadioListItem requires RadioList context; wrap it so the preview doesn't throw.
13
13
  playground: {
14
14
  defaults: {value: 'option-1', label: 'Option'},
@@ -97,7 +97,8 @@ export const docsZh = {
97
97
  name: 'RadioListItem',
98
98
  isHiddenFromOverview: true,
99
99
  displayName: 'Radio List Item',
100
- description: '单个单选选项,包含标签、描述和内容插槽。',
100
+ description:
101
+ '单个单选选项,包含标签、描述和内容插槽。在粗指针(触摸)下,已启用选项的目标通过不可见的点击区域向 44 px 扩展。它不小于所在行,与相邻控件较近时止于间距的一半;行尺寸不变。该点击区域是行的 `::after`,因此请用子元素装饰行。',
101
102
  props: [
102
103
  {
103
104
  name: 'label',
@@ -18,7 +18,7 @@
18
18
  * - /packages/cli/assets/templates/blocks/components/RadioList/ (showcase blocks)
19
19
  */
20
20
 
21
- import React, {use, useId, useRef, type ReactNode} from 'react';
21
+ import React, {use, useCallback, useId, useRef, type ReactNode} from 'react';
22
22
  import * as stylex from '@stylexjs/stylex';
23
23
  import type {StyleXStyles} from '@stylexjs/stylex';
24
24
  import type {BaseProps} from '../BaseProps';
@@ -32,6 +32,9 @@ import {rtlStyles} from '../utils/rtlStyles';
32
32
  import {indicatorScope} from '../Indicator/indicator.markers.stylex';
33
33
  import {useIndicatorFocusRing} from '../hooks/useIndicatorFocusRing';
34
34
  import {usePressFeedback} from '../hooks/usePressFeedback';
35
+ import {useMergedRefs} from '../hooks/useMergedRefs';
36
+ import {watchHitArea, writeHitInsets} from '../utils/hitArea';
37
+ import {touchHitAreaStyles} from '../utils/touchHitArea.stylex';
35
38
  import {useIndicator} from '../Indicator/useIndicator';
36
39
  import {Item, type ItemAlign} from '../Item/Item';
37
40
  import {themeProps} from '../utils/themeProps';
@@ -305,6 +308,21 @@ export function RadioListItem({
305
308
  // `<label htmlFor>` would double-fire under delegation.
306
309
  const radioRef = useRef<HTMLInputElement>(null);
307
310
 
311
+ // Under a coarse pointer an option's row grows toward 44 px through an
312
+ // invisible band, and the row keeps its size. A tap on the band lands on
313
+ // the row, which hands it to the radio. The watch lives as long as the row
314
+ // is attached: the ref's cleanup stops it.
315
+ const watchRowHitArea = useCallback(
316
+ (row: HTMLDivElement | null) => {
317
+ if (row == null || isDisabled) {
318
+ return;
319
+ }
320
+ return watchHitArea(row, insets => writeHitInsets(row, insets));
321
+ },
322
+ [isDisabled],
323
+ );
324
+ const mergedRowRef = useMergedRefs(ref, watchRowHitArea);
325
+
308
326
  const radioCircle = (
309
327
  <div
310
328
  {...stylex.props(
@@ -387,7 +405,7 @@ export function RadioListItem({
387
405
 
388
406
  return (
389
407
  <Item
390
- ref={ref}
408
+ ref={mergedRowRef}
391
409
  {...(isDisabled ? undefined : pressable)}
392
410
  startContent={mediaContent}
393
411
  // Delegate row-surface clicks (label text, description, and the empty
@@ -419,6 +437,7 @@ export function RadioListItem({
419
437
  // still overrides it from the higher `astryx-theme` layer.
420
438
  rowStyles.root,
421
439
  !isDisabled && rowStyles.press,
440
+ !isDisabled && touchHitAreaStyles.layer,
422
441
  xstyle,
423
442
  ] as StyleXStyles
424
443
  }
@@ -55,7 +55,12 @@ export interface PanGestureRecognizerOptions extends GestureRecognizerOptions<Pa
55
55
  minimumNumberOfTouches?: number;
56
56
  /** @default Infinity */
57
57
  maximumNumberOfTouches?: number;
58
- hysteresis?: number;
58
+ /**
59
+ * The travel that begins the pan, px. A function gives each press its own
60
+ * threshold: it reads the press's first touch when that touch lands.
61
+ * @default UIKit's 10
62
+ */
63
+ hysteresis?: number | ((touch: GestureTouch) => number);
59
64
  /** Movement that may begin the pan; anything else fails it. @default 'any' */
60
65
  direction?: PanDirection;
61
66
  /**
@@ -117,6 +122,11 @@ export class PanGestureRecognizer extends GestureRecognizer {
117
122
  travelAtBegan: Point = ZERO;
118
123
 
119
124
  private readonly tracker: VelocityTracker;
125
+ /**
126
+ * Read once per press. A finger that lands later keeps the press's
127
+ * threshold.
128
+ */
129
+ private readonly hysteresisFor: ((touch: GestureTouch) => number) | null;
120
130
  /** The lift's event time once the pan ended: its release (AST-100 FR2). */
121
131
  private releaseTime: number | undefined = undefined;
122
132
  /** The centroid where the gesture started, in client px. */
@@ -132,7 +142,10 @@ export class PanGestureRecognizer extends GestureRecognizer {
132
142
  this.minimumNumberOfTouches = options.minimumNumberOfTouches ?? 1;
133
143
  this.maximumNumberOfTouches =
134
144
  options.maximumNumberOfTouches ?? Number.POSITIVE_INFINITY;
135
- this.hysteresis = options.hysteresis ?? UIKIT_PAN.hysteresis.value;
145
+ const {hysteresis} = options;
146
+ this.hysteresisFor = typeof hysteresis === 'function' ? hysteresis : null;
147
+ this.hysteresis =
148
+ typeof hysteresis === 'number' ? hysteresis : UIKIT_PAN.hysteresis.value;
136
149
  this.direction = options.direction ?? 'any';
137
150
  this.axisRatio = options.axisRatio ?? 1;
138
151
  this.lockAxis = options.lockAxis ?? false;
@@ -237,6 +250,10 @@ export class PanGestureRecognizer extends GestureRecognizer {
237
250
  event: GestureEventContext,
238
251
  ): void {
239
252
  if (this.numberOfTouches === touches.length) {
253
+ const first = touches[0];
254
+ if (this.hysteresisFor != null && first != null) {
255
+ this.hysteresis = this.hysteresisFor(first);
256
+ }
240
257
  this.origin = centroid(this.touches.values());
241
258
  this.adjust = ZERO;
242
259
  this.raw = ZERO;
@@ -118,7 +118,8 @@ The pan reads its numbers the way `UIPanGestureRecognizer` does, as measured
118
118
  in the UIKit lab:
119
119
 
120
120
  - It begins once the centroid has moved `hysteresis` (10 px) along either
121
- axis, not as a distance.
121
+ axis, not as a distance. A function gives each press its own threshold,
122
+ read from the press's first touch when it lands.
122
123
  - `translation()` leaves out the travel the threshold took: a slow drag reads
123
124
  zero when the pan begins, and a step that overshoots keeps only the
124
125
  overshoot. A dragged surface does not jump. `totalTranslation()` is the
@@ -76,12 +76,17 @@ interface EdgeLayout {
76
76
  */
77
77
  export interface ScreenEdgePanGestureRecognizerOptions extends Omit<
78
78
  PanGestureRecognizerOptions,
79
- 'direction' | 'allowedPointerTypes' | 'allowedScrollTypes'
79
+ 'direction' | 'allowedPointerTypes' | 'allowedScrollTypes' | 'hysteresis'
80
80
  > {
81
81
  /** The edges a touch may start from. @default ['leading'] */
82
82
  edges?: ReadonlyArray<ScreenEdge>;
83
83
  /** Width of the band at each edge, px. */
84
84
  edgeRegion?: number;
85
+ /**
86
+ * The travel that begins the pan in its edge zone, px. It is one number for
87
+ * every press, since the zone sets the threshold itself. @default UIKit's 15
88
+ */
89
+ hysteresis?: number;
85
90
  }
86
91
 
87
92
  export class ScreenEdgePanGestureRecognizer extends PanGestureRecognizer {
@@ -0,0 +1,41 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file touchHitArea.stylex.ts
5
+ * @input Uses StyleX and the insets that utils/hitArea.ts writes on a host
6
+ * @output Exports `touchHitAreaStyles.layer`, a host's invisible hit band
7
+ * under a coarse pointer
8
+ * @position Internal styling utility for CheckboxInput and RadioListItem
9
+ *
10
+ * The band is the host's `::after`, grown past each edge by the measured
11
+ * inset. A clip path cuts out the host's box, so its content keeps its taps.
12
+ * A tap on the band lands on the host. SYNC: hitArea.ts HIT_INSET_PROPERTIES.
13
+ */
14
+
15
+ import * as stylex from '@stylexjs/stylex';
16
+
17
+ // StyleX reads these at build time, so they are spelled out.
18
+ const TOUCH = '@media (pointer: coarse)';
19
+ // The insets are measured in the viewport, so the sides are physical.
20
+ const TOP = 'var(--astryx-hit-inset-top, 0px)';
21
+ const RIGHT = 'var(--astryx-hit-inset-right, 0px)';
22
+ const BOTTOM = 'var(--astryx-hit-inset-bottom, 0px)';
23
+ const LEFT = 'var(--astryx-hit-inset-left, 0px)';
24
+
25
+ export const touchHitAreaStyles = stylex.create({
26
+ layer: {
27
+ '::after': {
28
+ content: {default: null, [TOUCH]: '""'},
29
+ position: {default: null, [TOUCH]: 'absolute'},
30
+ inset: {
31
+ default: null,
32
+ [TOUCH]: `calc(${TOP} * -1) calc(${RIGHT} * -1) calc(${BOTTOM} * -1) calc(${LEFT} * -1)`,
33
+ },
34
+ // The outer rectangle, then the host's box as an even-odd hole.
35
+ clipPath: {
36
+ default: null,
37
+ [TOUCH]: `polygon(evenodd, 0 0, 100% 0, 100% 100%, 0 100%, 0 0, ${LEFT} ${TOP}, calc(100% - ${RIGHT}) ${TOP}, calc(100% - ${RIGHT}) calc(100% - ${BOTTOM}), ${LEFT} calc(100% - ${BOTTOM}), ${LEFT} ${TOP})`,
38
+ },
39
+ },
40
+ },
41
+ });