@gryt/ui-native 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/components/AlertDialog/AlertDialog.d.ts.map +1 -1
  2. package/dist/components/Checkbox/Checkbox.d.ts +7 -1
  3. package/dist/components/Checkbox/Checkbox.d.ts.map +1 -1
  4. package/dist/components/Checkbox/Checkbox.js +47 -15
  5. package/dist/components/Dialog/Dialog.d.ts.map +1 -1
  6. package/dist/components/Dialog/Dialog.js +10 -1
  7. package/dist/components/Drawer/Drawer.d.ts.map +1 -1
  8. package/dist/components/Drawer/Drawer.js +28 -3
  9. package/dist/components/Radio/Radio.d.ts +9 -1
  10. package/dist/components/Radio/Radio.d.ts.map +1 -1
  11. package/dist/components/Radio/Radio.js +38 -14
  12. package/dist/components/ScrollArea/ScrollArea.d.ts +6 -4
  13. package/dist/components/ScrollArea/ScrollArea.d.ts.map +1 -1
  14. package/dist/components/ScrollArea/ScrollArea.js +19 -4
  15. package/dist/components/Sheet/Sheet.d.ts +90 -0
  16. package/dist/components/Sheet/Sheet.d.ts.map +1 -0
  17. package/dist/components/Sheet/Sheet.js +133 -0
  18. package/dist/components/Slider/Slider.d.ts.map +1 -1
  19. package/dist/components/Slider/Slider.js +33 -0
  20. package/dist/components/Switch/Switch.d.ts +4 -1
  21. package/dist/components/Switch/Switch.d.ts.map +1 -1
  22. package/dist/components/Switch/Switch.js +21 -17
  23. package/dist/components/Tabs/Tabs.d.ts +38 -13
  24. package/dist/components/Tabs/Tabs.d.ts.map +1 -1
  25. package/dist/components/Tabs/Tabs.js +157 -42
  26. package/dist/components/Toggle/Toggle.d.ts +0 -1
  27. package/dist/components/Toggle/Toggle.d.ts.map +1 -1
  28. package/dist/components/Toggle/Toggle.js +15 -1
  29. package/dist/components/internal/ControlRow.d.ts +54 -0
  30. package/dist/components/internal/ControlRow.d.ts.map +1 -0
  31. package/dist/components/internal/ControlRow.js +59 -0
  32. package/dist/components/internal/dragLock.d.ts +43 -0
  33. package/dist/components/internal/dragLock.d.ts.map +1 -0
  34. package/dist/components/internal/dragLock.js +18 -0
  35. package/dist/index.d.ts +2 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +2 -0
  38. package/package.json +9 -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;;;;;gBAAk6H,CAAC;aAAkB,CAAC;;;gBAAiS,CAAC;aAAkB,CAAC;;;gBAAkiB,CAAC;aAAkB,CAAC;;CADjoL,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;;;;;gBAAovK,CAAC;aAAkB,CAAC;;;gBAAiS,CAAC;aAAkB,CAAC;;;gBAAkiB,CAAC;aAAkB,CAAC;;CADn9N,CAAC"}
@@ -1,3 +1,4 @@
1
+ import { type ReactNode } from "react";
1
2
  import { type StyleProp, type ViewStyle } from "react-native";
2
3
  export type CheckboxTone = "primary" | "secondary" | "neutral" | "danger";
3
4
  export interface CheckboxProps {
@@ -8,6 +9,11 @@ export interface CheckboxProps {
8
9
  onCheckedChange?: (checked: boolean) => void;
9
10
  disabled?: boolean;
10
11
  tone?: CheckboxTone;
12
+ /**
13
+ * Tapping this toggles the box, which is what a `<label>` does on the web.
14
+ * See ControlRow for why it is a prop here rather than a wrapper element.
15
+ */
16
+ label?: ReactNode;
11
17
  style?: StyleProp<ViewStyle>;
12
18
  accessibilityLabel?: string;
13
19
  }
@@ -20,5 +26,5 @@ export interface CheckboxProps {
20
26
  * react-native-svg on top. Both are decisions for whoever needs a full icon set,
21
27
  * not for a checkbox.
22
28
  */
23
- export declare function Checkbox({ checked: controlled, defaultChecked, indeterminate, onCheckedChange, disabled, tone, style, accessibilityLabel, }: CheckboxProps): import("react").JSX.Element;
29
+ export declare function Checkbox({ checked: controlled, defaultChecked, indeterminate, onCheckedChange, disabled, tone, label, style, accessibilityLabel, }: CheckboxProps): import("react").JSX.Element;
24
30
  //# sourceMappingURL=Checkbox.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Checkbox.d.ts","sourceRoot":"","sources":["../../../src/components/Checkbox/Checkbox.tsx"],"names":[],"mappings":"AACA,OAAO,EAAmB,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAI/E,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC;AAE1E,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,sEAAsE;IACtE,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IAC7C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAID;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,EACvB,OAAO,EAAE,UAAU,EACnB,cAAsB,EACtB,aAAqB,EACrB,eAAe,EACf,QAAQ,EACR,IAAgB,EAChB,KAAK,EACL,kBAAkB,GACnB,EAAE,aAAa,+BA2Cf"}
1
+ {"version":3,"file":"Checkbox.d.ts","sourceRoot":"","sources":["../../../src/components/Checkbox/Checkbox.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC5D,OAAO,EAAQ,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AASpE,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC;AAE1E,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,sEAAsE;IACtE,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IAC7C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB;;;OAGG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAID;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,EACvB,OAAO,EAAE,UAAU,EACnB,cAAsB,EACtB,aAAqB,EACrB,eAAe,EACf,QAAQ,EACR,IAAgB,EAChB,KAAK,EACL,KAAK,EACL,kBAAkB,GACnB,EAAE,aAAa,+BAiFf"}
@@ -1,7 +1,12 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useState } from "react";
3
- import { Pressable, Text } from "react-native";
2
+ import { useEffect, useState } from "react";
3
+ import { Text } 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 { useReducedMotion } from "../../hooks/useReducedMotion.js";
9
+ import { ControlRow } from "../internal/ControlRow.js";
5
10
  const SIZE = 20;
6
11
  /**
7
12
  * The tick is a text glyph rather than an icon.
@@ -12,32 +17,59 @@ const SIZE = 20;
12
17
  * react-native-svg on top. Both are decisions for whoever needs a full icon set,
13
18
  * not for a checkbox.
14
19
  */
15
- export function Checkbox({ checked: controlled, defaultChecked = false, indeterminate = false, onCheckedChange, disabled, tone = "primary", style, accessibilityLabel, }) {
20
+ export function Checkbox({ checked: controlled, defaultChecked = false, indeterminate = false, onCheckedChange, disabled, tone = "primary", label, style, accessibilityLabel, }) {
16
21
  const theme = useTheme();
22
+ const reducedMotion = useReducedMotion();
17
23
  const [uncontrolled, setUncontrolled] = useState(defaultChecked);
18
24
  const checked = controlled ?? uncontrolled;
19
25
  const ramp = toneRamp(theme, tone);
20
26
  const filled = checked || indeterminate;
21
- return (_jsx(Pressable, { accessibilityRole: "checkbox", accessibilityState: {
22
- checked: indeterminate ? "mixed" : checked,
23
- disabled: !!disabled,
24
- }, accessibilityLabel: accessibilityLabel, disabled: disabled, onPress: () => {
27
+ /**
28
+ * The tick scales from 0, not from something near 1.
29
+ *
30
+ * Straight from the web's comment, and it is not a detail: the spring's
31
+ * overshoot is a percentage of the travel, so 0 → 1 overshoots to 1.12 and
32
+ * visibly springs, while 0.95 → 1 overshoots by 0.006 and does nothing at
33
+ * all. It used to appear instantly here, which is the same bug with the
34
+ * travel set to zero.
35
+ */
36
+ const tick = useSharedValue(filled ? 1 : 0);
37
+ useEffect(() => {
38
+ const to = filled ? 1 : 0;
39
+ // eslint-disable-next-line react-hooks/immutability
40
+ tick.value = reducedMotion ? to : springy(to);
41
+ }, [filled, tick, reducedMotion]);
42
+ const tickStyle = useAnimatedStyle(() => ({
43
+ transform: [{ scale: tick.value }],
44
+ opacity: tick.value,
45
+ }));
46
+ return (_jsx(ControlRow, { label: label, onPress: () => {
25
47
  const next = !checked;
26
48
  if (controlled === undefined)
27
49
  setUncontrolled(next);
28
50
  onCheckedChange?.(next);
29
- }, style: [
30
- {
51
+ }, disabled: disabled, pressScale: grytScaleSteps.checkbox.press, accessibilityRole: "checkbox", accessibilityState: {
52
+ checked: indeterminate ? "mixed" : checked,
53
+ disabled: !!disabled,
54
+ }, accessibilityLabel: accessibilityLabel, style: style, children: _jsx(Animated.View, { style: {
31
55
  width: SIZE,
32
56
  height: SIZE,
33
57
  borderRadius: theme.radius.sm,
34
58
  alignItems: "center",
35
59
  justifyContent: "center",
36
- borderWidth: filled ? 0 : 1.5,
37
- borderColor: theme.color.border,
38
- backgroundColor: filled ? ramp[8] : "transparent",
60
+ // The web keeps the border at 1 always and makes it transparent when
61
+ // checked, rather than dropping to 0 — a border that disappears
62
+ // changes the box's size mid-transition.
63
+ borderWidth: 1,
64
+ borderColor: filled ? "transparent" : theme.color.border,
65
+ // Was `transparent`, which is not what the web does: an unchecked box
66
+ // is a filled surface with an outline, not a hole.
67
+ backgroundColor: filled ? ramp[8] : theme.color.surfaceRaised,
39
68
  opacity: disabled ? 0.5 : 1,
40
- },
41
- style,
42
- ], children: filled ? (_jsx(Text, { style: { color: theme.color.bg, fontSize: 13, fontWeight: "900", lineHeight: 15 }, children: indeterminate ? "–" : "✓" })) : null }));
69
+ }, children: _jsx(Animated.View, { style: tickStyle, children: _jsx(Text, { style: {
70
+ color: theme.color.onAccent,
71
+ fontSize: 13,
72
+ fontWeight: "900",
73
+ lineHeight: 15,
74
+ }, children: indeterminate ? "–" : "✓" }) }) }) }));
43
75
  }
@@ -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,+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"}
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,+BA4GlB;AAYD;;;;;;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"}
@@ -81,7 +81,16 @@ function Popup({ children, style, dismissible = true, scrollable = true, }) {
81
81
  justifyContent: "center",
82
82
  padding: theme.space(5),
83
83
  backgroundColor: "rgba(0,0,0,0.6)",
84
- }, children: _jsx(Pressable, { onPress: () => { }, style: { width: "100%", alignItems: "center", maxHeight: "80%" }, children: body }) }) }));
84
+ }, children: _jsx(View, { onStartShouldSetResponder: claimTouch, style: { width: "100%", alignItems: "center", maxHeight: "80%" }, children: body }) }) }));
85
+ }
86
+ /**
87
+ * Claim a touch that no child wanted.
88
+ *
89
+ * Declared once rather than as an inline arrow, so every Dialog is not handing
90
+ * the responder system a new function identity on every render.
91
+ */
92
+ function claimTouch() {
93
+ return true;
85
94
  }
86
95
  /**
87
96
  * Not rendered.
@@ -1 +1 @@
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
+ {"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;AAStB,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,+BAqGlB;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"}
@@ -3,6 +3,7 @@ import { createContext, useContext, useEffect } from "react";
3
3
  import { Dimensions, Modal, Pressable } from "react-native";
4
4
  import Animated, { useAnimatedStyle, useSharedValue } from "react-native-reanimated";
5
5
  import { useReducedMotion } from "../../hooks/useReducedMotion.js";
6
+ import { grytDrawerBleed } from "@gryt/theme";
6
7
  import { durations, springy } from "../../motion/index.js";
7
8
  import { useOpenState } from "../../overlay/useOpenState.js";
8
9
  import { useTheme } from "../../theme/index.js";
@@ -39,7 +40,26 @@ function Popup({ children, side = "left", size = 0.8, dismissible = true, style
39
40
  const vertical = side === "bottom";
40
41
  const extent = vertical ? screen.height * size : screen.width * size;
41
42
  const progress = useSharedValue(0);
42
- const hidden = side === "right" ? extent : side === "left" ? -extent : extent;
43
+ /**
44
+ * The panel is built `grytDrawerBleed` larger than it needs to be and hangs
45
+ * that much off-screen, with matching padding so the content sits where it
46
+ * would have.
47
+ *
48
+ * The spring overshoots — that is what makes it a spring — and it settles
49
+ * onto its target from both directions. A panel sized exactly to its resting
50
+ * place therefore shows a seam of backdrop down its edge on the undershoot,
51
+ * for a frame or two, every time it opens. The web has had this since the
52
+ * Drawer was written, as `--gryt-drawer-bleed`; this side did not.
53
+ *
54
+ * `grytDrawerBleed` rather than a local 64, and `bleedTokens.test.ts` in
55
+ * @gryt/ui keeps it equal to what theme.css says. Two overhangs that must
56
+ * match is how they stop matching.
57
+ */
58
+ const bleed = grytDrawerBleed;
59
+ const panelExtent = extent + bleed;
60
+ // Measured against the bleed panel, so the panel's *visible* edge lands
61
+ // exactly off-screen rather than the overhang doing it.
62
+ const hidden = side === "right" ? panelExtent : side === "left" ? -panelExtent : panelExtent;
43
63
  // 700ms, the web's `--gryt-dur-spring-soft`, on the overshooting curve —
44
64
  // which is what the web uses here despite the panel travelling its own
45
65
  // width. Matching it rather than substituting the tight curve, because the
@@ -68,8 +88,13 @@ function Popup({ children, side = "left", size = 0.8, dismissible = true, style
68
88
  bottom: 0,
69
89
  left: side === "right" ? undefined : 0,
70
90
  right: side === "left" ? undefined : 0,
71
- width: vertical ? "100%" : extent,
72
- height: vertical ? extent : "100%",
91
+ width: vertical ? "100%" : panelExtent,
92
+ height: vertical ? panelExtent : "100%",
93
+ // The overhang, and the padding that puts the content back where
94
+ // it would have been without it.
95
+ paddingLeft: side === "right" ? bleed : 0,
96
+ paddingRight: side === "left" ? bleed : 0,
97
+ paddingBottom: vertical ? bleed : 0,
73
98
  backgroundColor: theme.color.surface,
74
99
  borderColor: theme.color.border,
75
100
  borderRightWidth: side === "left" ? 1 : 0,
@@ -21,8 +21,16 @@ export interface RadioProps {
21
21
  value: string | number;
22
22
  disabled?: boolean;
23
23
  tone?: RadioTone;
24
+ /**
25
+ * Tapping this selects the radio, which is what a `<label>` does on the web.
26
+ *
27
+ * Radios need it more than anything else here: they come in stacks, so a
28
+ * 20pt target missed by a few points does not do nothing — it selects the
29
+ * neighbour, which is worse than no response at all.
30
+ */
31
+ label?: ReactNode;
24
32
  style?: StyleProp<ViewStyle>;
25
33
  accessibilityLabel?: string;
26
34
  }
27
- export declare function Radio({ value, disabled, tone, style, accessibilityLabel, }: RadioProps): import("react").JSX.Element;
35
+ export declare function Radio({ value, disabled, tone, label, style, accessibilityLabel, }: RadioProps): import("react").JSX.Element;
28
36
  //# sourceMappingURL=Radio.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Radio.d.ts","sourceRoot":"","sources":["../../../src/components/Radio/Radio.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAuC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC5E,OAAO,EAAmB,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAI/E,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC;AAUvE,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,KAAK,IAAI,CAAC;IACjD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,EACzB,KAAK,EAAE,UAAU,EACjB,YAAY,EACZ,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,KAAK,GACN,EAAE,eAAe,+BAsBjB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAID,wBAAgB,KAAK,CAAC,EACpB,KAAK,EACL,QAAQ,EACR,IAAgB,EAChB,KAAK,EACL,kBAAkB,GACnB,EAAE,UAAU,+BA0CZ"}
1
+ {"version":3,"file":"Radio.d.ts","sourceRoot":"","sources":["../../../src/components/Radio/Radio.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAkD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACvF,OAAO,EAAQ,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AASpE,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC;AAUvE,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,KAAK,IAAI,CAAC;IACjD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,EACzB,KAAK,EAAE,UAAU,EACjB,YAAY,EACZ,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,KAAK,GACN,EAAE,eAAe,+BAsBjB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAID,wBAAgB,KAAK,CAAC,EACpB,KAAK,EACL,QAAQ,EACR,IAAgB,EAChB,KAAK,EACL,KAAK,EACL,kBAAkB,GACnB,EAAE,UAAU,+BAoEZ"}
@@ -1,7 +1,12 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { createContext, useContext, useState } from "react";
3
- import { Pressable, View } from "react-native";
2
+ import { createContext, useContext, useEffect, useState } from "react";
3
+ import { View } from "react-native";
4
+ import Animated, { useAnimatedStyle, useSharedValue } from "react-native-reanimated";
5
+ import { grytScaleSteps } from "@gryt/theme";
6
+ import { useReducedMotion } from "../../hooks/useReducedMotion.js";
7
+ import { springy } from "../../motion/index.js";
4
8
  import { toneRamp, useTheme } from "../../theme/index.js";
9
+ import { ControlRow } from "../internal/ControlRow.js";
5
10
  const RadioGroupContext = createContext(null);
6
11
  /**
7
12
  * The group holds the value, which is the part a radio cannot do alone.
@@ -24,30 +29,49 @@ export function RadioGroup({ value: controlled, defaultValue, onValueChange, dis
24
29
  }, children: _jsx(View, { accessibilityRole: "radiogroup", style: style, children: children }) }));
25
30
  }
26
31
  const SIZE = 20;
27
- export function Radio({ value, disabled, tone = "primary", style, accessibilityLabel, }) {
32
+ export function Radio({ value, disabled, tone = "primary", label, style, accessibilityLabel, }) {
28
33
  const theme = useTheme();
34
+ const reducedMotion = useReducedMotion();
29
35
  const group = useContext(RadioGroupContext);
30
36
  if (!group)
31
37
  throw new Error("Radio must be rendered inside a RadioGroup.");
32
38
  const selected = group.value === value;
33
39
  const isDisabled = disabled || group.disabled;
34
40
  const ramp = toneRamp(theme, tone);
35
- return (_jsx(Pressable, { accessibilityRole: "radio", accessibilityState: { checked: selected, disabled: !!isDisabled }, accessibilityLabel: accessibilityLabel, disabled: isDisabled, onPress: () => group.setValue(value), style: [
36
- {
41
+ // Scales from 0 for the same reason the checkbox tick does see the comment
42
+ // there. The dot used to be mounted and unmounted, which is that transition
43
+ // with the duration set to nothing.
44
+ const dot = useSharedValue(selected ? 1 : 0);
45
+ useEffect(() => {
46
+ const to = selected ? 1 : 0;
47
+ // eslint-disable-next-line react-hooks/immutability
48
+ dot.value = reducedMotion ? to : springy(to);
49
+ }, [selected, dot, reducedMotion]);
50
+ const dotStyle = useAnimatedStyle(() => ({
51
+ transform: [{ scale: dot.value }],
52
+ opacity: dot.value,
53
+ }));
54
+ return (_jsx(ControlRow, { label: label, onPress: () => group.setValue(value), disabled: isDisabled, pressScale: grytScaleSteps.radio.press, accessibilityRole: "radio", accessibilityState: { checked: selected, disabled: !!isDisabled }, accessibilityLabel: accessibilityLabel, style: style, children: _jsx(View, { style: {
37
55
  width: SIZE,
38
56
  height: SIZE,
39
57
  borderRadius: SIZE / 2,
40
58
  alignItems: "center",
41
59
  justifyContent: "center",
42
- borderWidth: 1.5,
60
+ // 1, matching the web. It was 1.5, which reads heavier than the
61
+ // checkbox sitting next to it in the same form.
62
+ borderWidth: 1,
43
63
  borderColor: selected ? ramp[8] : theme.color.border,
64
+ // The web fills the circle and outlines it; only the dot is new when
65
+ // it is selected.
66
+ backgroundColor: theme.color.surfaceRaised,
44
67
  opacity: isDisabled ? 0.5 : 1,
45
- },
46
- style,
47
- ], children: selected ? (_jsx(View, { style: {
48
- width: SIZE / 2,
49
- height: SIZE / 2,
50
- borderRadius: SIZE / 4,
51
- backgroundColor: ramp[8],
52
- } })) : null }));
68
+ }, children: _jsx(Animated.View, { style: [
69
+ {
70
+ width: SIZE / 2,
71
+ height: SIZE / 2,
72
+ borderRadius: SIZE / 4,
73
+ backgroundColor: ramp[8],
74
+ },
75
+ dotStyle,
76
+ ] }) }) }));
53
77
  }
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from "react";
1
+ import { type ReactNode } from "react";
2
2
  import { type ScrollViewProps, type StyleProp, type ViewStyle } from "react-native";
3
3
  export interface ScrollAreaProps extends Omit<ScrollViewProps, "style"> {
4
4
  children?: ReactNode;
@@ -15,8 +15,10 @@ export interface ScrollAreaProps extends Omit<ScrollViewProps, "style"> {
15
15
  * device. So the component stays for call-site parity and does almost nothing,
16
16
  * which is the honest version.
17
17
  *
18
- * Its one job is defaulting the indicator off, since the web version hides the
19
- * native scrollbar too.
18
+ * Its one job was defaulting the indicator off, since the web version hides the
19
+ * native scrollbar too. It has a second now: it publishes a drag lock, so a
20
+ * control inside it can hold it still while it is being dragged. See
21
+ * internal/dragLock — a Slider is unusable in a scrolling screen without it.
20
22
  */
21
- export declare function ScrollArea({ children, horizontal, style, contentStyle, showsVerticalScrollIndicator, showsHorizontalScrollIndicator, ...rest }: ScrollAreaProps): import("react").JSX.Element;
23
+ export declare function ScrollArea({ children, horizontal, style, contentStyle, showsVerticalScrollIndicator, showsHorizontalScrollIndicator, scrollEnabled, ...rest }: ScrollAreaProps): import("react").JSX.Element;
22
24
  //# sourceMappingURL=ScrollArea.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ScrollArea.d.ts","sourceRoot":"","sources":["../../../src/components/ScrollArea/ScrollArea.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,EAAc,KAAK,eAAe,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAEhG,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAAC,eAAe,EAAE,OAAO,CAAC;IACrE,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,YAAY,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CACrC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,EACzB,QAAQ,EACR,UAAkB,EAClB,KAAK,EACL,YAAY,EACZ,4BAAoC,EACpC,8BAAsC,EACtC,GAAG,IAAI,EACR,EAAE,eAAe,+BAajB"}
1
+ {"version":3,"file":"ScrollArea.d.ts","sourceRoot":"","sources":["../../../src/components/ScrollArea/ScrollArea.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAkC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACvE,OAAO,EAAc,KAAK,eAAe,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAIhG,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAAC,eAAe,EAAE,OAAO,CAAC;IACrE,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,YAAY,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CACrC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,UAAU,CAAC,EACzB,QAAQ,EACR,UAAkB,EAClB,KAAK,EACL,YAAY,EACZ,4BAAoC,EACpC,8BAAsC,EACtC,aAAa,EACb,GAAG,IAAI,EACR,EAAE,eAAe,+BAgCjB"}
@@ -1,5 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCallback, useMemo, useState } from "react";
2
3
  import { ScrollView } from "react-native";
4
+ import { DragLockContext } from "../internal/dragLock.js";
3
5
  /**
4
6
  * A thin pass over ScrollView.
5
7
  *
@@ -9,9 +11,22 @@ import { ScrollView } from "react-native";
9
11
  * device. So the component stays for call-site parity and does almost nothing,
10
12
  * which is the honest version.
11
13
  *
12
- * Its one job is defaulting the indicator off, since the web version hides the
13
- * native scrollbar too.
14
+ * Its one job was defaulting the indicator off, since the web version hides the
15
+ * native scrollbar too. It has a second now: it publishes a drag lock, so a
16
+ * control inside it can hold it still while it is being dragged. See
17
+ * internal/dragLock — a Slider is unusable in a scrolling screen without it.
14
18
  */
15
- export function ScrollArea({ children, horizontal = false, style, contentStyle, showsVerticalScrollIndicator = false, showsHorizontalScrollIndicator = false, ...rest }) {
16
- return (_jsx(ScrollView, { horizontal: horizontal, style: style, contentContainerStyle: contentStyle, showsVerticalScrollIndicator: showsVerticalScrollIndicator, showsHorizontalScrollIndicator: showsHorizontalScrollIndicator, ...rest, children: children }));
19
+ export function ScrollArea({ children, horizontal = false, style, contentStyle, showsVerticalScrollIndicator = false, showsHorizontalScrollIndicator = false, scrollEnabled, ...rest }) {
20
+ const [locks, setLocks] = useState(0);
21
+ // Counted rather than a boolean. Two controls can overlap in principle — a
22
+ // drag interrupted by a second one starting — and the first to finish would
23
+ // otherwise unlock the list while the second is still going.
24
+ const lock = useCallback(() => setLocks((n) => n + 1), []);
25
+ const unlock = useCallback(() => setLocks((n) => Math.max(0, n - 1)), []);
26
+ const dragLock = useMemo(() => ({ locked: locks > 0, lock, unlock }), [locks, lock, unlock]);
27
+ return (_jsx(DragLockContext.Provider, { value: dragLock, children: _jsx(ScrollView, { horizontal: horizontal, style: style, contentContainerStyle: contentStyle, showsVerticalScrollIndicator: showsVerticalScrollIndicator, showsHorizontalScrollIndicator: showsHorizontalScrollIndicator,
28
+ // Only overridden while something is actually dragging, so a caller
29
+ // that passes `scrollEnabled={false}` still gets a list that never
30
+ // scrolls.
31
+ scrollEnabled: scrollEnabled === false ? false : locks === 0, ...rest, children: children }) }));
17
32
  }
@@ -0,0 +1,90 @@
1
+ import { type ReactNode } from "react";
2
+ import { type StyleProp, type ViewStyle } from "react-native";
3
+ /**
4
+ * A bottom sheet, which is the phone's modal.
5
+ *
6
+ * `@gryt/ui` has no counterpart, and that is deliberate rather than an
7
+ * oversight to be corrected later. A sheet is what a phone does where the web
8
+ * opens a dialog or slides a drawer, and the two are not the same interaction:
9
+ * a sheet is dragged, it settles at heights the user chooses, and dismissing it
10
+ * is a flick rather than a click on an X. Shipping the web's Dialog on a phone
11
+ * would be 1:1 and wrong; inventing a sheet on the web would be worse. So this
12
+ * is an addition, and it is in the parity exceptions table as one.
13
+ *
14
+ * Built on `@gorhom/bottom-sheet` rather than by hand, which is the opposite of
15
+ * the call made for Slider and Tabs, and the reason is the shape of the
16
+ * problem. Those were shallow and Gryt-specific — a press scale is four lines
17
+ * and no library will give you the right one. A sheet is deep and generic:
18
+ * velocity-based dismiss, snap points, keyboard avoidance, a backdrop whose
19
+ * opacity tracks the drag, and the handoff between dragging the sheet and
20
+ * scrolling the content inside it. None of that is about Gryt and all of it is
21
+ * months of edge cases.
22
+ *
23
+ * The library supplies behaviour only. Every colour, radius and dimension below
24
+ * comes from the theme.
25
+ *
26
+ * **`SheetProvider` has to be mounted above anything that renders a Sheet**,
27
+ * next to `ToastProvider` and `TooltipProvider`. That is not ceremony: this is
28
+ * `BottomSheetModal`, not `BottomSheet`, and the difference is where the sheet
29
+ * is anchored.
30
+ *
31
+ * `BottomSheet` positions itself inside whatever container it is rendered in.
32
+ * Written inline in a screen, as this was first, it anchors to its slot in the
33
+ * layout — so it appeared partway down the page with the content behind it
34
+ * showing through, and read as entering from the top. `BottomSheetModal`
35
+ * portals to the provider instead, which sits at the app root, so it is always
36
+ * anchored to the bottom of the screen no matter where the call site is.
37
+ */
38
+ export interface SheetProviderProps {
39
+ children?: ReactNode;
40
+ }
41
+ /** Mount once, at the root. See the note above for why it is required. */
42
+ export declare function SheetProvider({ children }: SheetProviderProps): import("react").JSX.Element;
43
+ export interface SheetProps {
44
+ /**
45
+ * Heights the sheet settles at, as percentages or points.
46
+ *
47
+ * The web has no equivalent concept — a dialog is one size — which is part of
48
+ * why this is its own component rather than a Drawer with a prop.
49
+ */
50
+ snapPoints?: (string | number)[];
51
+ /** Starts open. Uncontrolled; use the Trigger for the usual case. */
52
+ defaultOpen?: boolean;
53
+ onOpenChange?: (open: boolean) => void;
54
+ children?: ReactNode;
55
+ }
56
+ declare function Root({ snapPoints, defaultOpen, onOpenChange, children, }: SheetProps): import("react").JSX.Element;
57
+ export interface SheetTriggerProps {
58
+ children?: ReactNode;
59
+ style?: StyleProp<ViewStyle>;
60
+ }
61
+ /**
62
+ * A Pressable, like every other trigger in this package — which means its
63
+ * children have to be plain visual content. Nesting a Button inside one gives
64
+ * the inner pressable the touch and the sheet never opens, silently. Same
65
+ * footgun the overlay triggers already carry.
66
+ */
67
+ declare function Trigger({ children, style }: SheetTriggerProps): import("react").JSX.Element;
68
+ export interface SheetContentProps {
69
+ children?: ReactNode;
70
+ style?: StyleProp<ViewStyle>;
71
+ }
72
+ declare function Content({ children, style }: SheetContentProps): import("react").JSX.Element;
73
+ export interface SheetCloseProps {
74
+ children?: ReactNode;
75
+ style?: StyleProp<ViewStyle>;
76
+ }
77
+ declare function Close({ children, style }: SheetCloseProps): import("react").JSX.Element;
78
+ export interface SheetTitleProps {
79
+ children?: ReactNode;
80
+ style?: StyleProp<ViewStyle>;
81
+ }
82
+ declare function Title({ children, style }: SheetTitleProps): import("react").JSX.Element;
83
+ export declare const Sheet: typeof Root & {
84
+ Trigger: typeof Trigger;
85
+ Content: typeof Content;
86
+ Close: typeof Close;
87
+ Title: typeof Title;
88
+ };
89
+ export {};
90
+ //# sourceMappingURL=Sheet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Sheet.d.ts","sourceRoot":"","sources":["../../../src/components/Sheet/Sheet.tsx"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,EAAyB,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAerF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE,kBAAkB,+BAE7D;AAeD,MAAM,WAAW,UAAU;IACzB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IACjC,qEAAqE;IACrE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACvC,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,iBAAS,IAAI,CAAC,EACZ,UAAoB,EACpB,WAAmB,EACnB,YAAY,EACZ,QAAQ,GACT,EAAE,UAAU,+BAyBZ;AAWD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED;;;;;GAKG;AACH,iBAAS,OAAO,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,iBAAiB,+BAOtD;AAED,MAAM,WAAW,iBAAiB;IAChC,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,iBAAiB,+BA+HtD;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED,iBAAS,KAAK,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,eAAe,+BAOlD;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED,iBAAS,KAAK,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,eAAe,+BAiBlD;AAED,eAAO,MAAM,KAAK;;;;;CAA0D,CAAC"}
@@ -0,0 +1,133 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, } from "react";
3
+ import { Pressable, Text, View } from "react-native";
4
+ import { BottomSheetBackdrop, BottomSheetModal, BottomSheetModalProvider, BottomSheetView, useBottomSheetTimingConfigs, } from "@gorhom/bottom-sheet";
5
+ import { grytDrawerBleed } from "@gryt/theme";
6
+ import { durations, easeSpring } from "../../motion/index.js";
7
+ import { useTheme } from "../../theme/index.js";
8
+ /** Mount once, at the root. See the note above for why it is required. */
9
+ export function SheetProvider({ children }) {
10
+ return _jsx(BottomSheetModalProvider, { children: children });
11
+ }
12
+ const SheetContext = createContext(null);
13
+ function useSheet(part) {
14
+ const value = useContext(SheetContext);
15
+ if (!value)
16
+ throw new Error(`Sheet.${part} must be rendered inside Sheet.`);
17
+ return value;
18
+ }
19
+ function Root({ snapPoints = ["50%"], defaultOpen = false, onOpenChange, children, }) {
20
+ const ref = useRef(null);
21
+ // `present` and `dismiss` rather than mounting and unmounting by hand: the
22
+ // modal is always rendered and the provider decides whether it is on screen,
23
+ // which is what removes the mount-then-snap-on-the-next-frame dance the
24
+ // inline version needed.
25
+ const open = useCallback(() => {
26
+ ref.current?.present();
27
+ onOpenChange?.(true);
28
+ }, [onOpenChange]);
29
+ const close = useCallback(() => {
30
+ ref.current?.dismiss();
31
+ }, []);
32
+ const context = useMemo(() => ({ open, close }), [open, close]);
33
+ return (_jsx(SheetContext.Provider, { value: context, children: _jsx(SheetRefContext.Provider, { value: { ref, defaultOpen, snapPoints, onOpenChange }, children: children }) }));
34
+ }
35
+ const SheetRefContext = createContext(null);
36
+ /**
37
+ * A Pressable, like every other trigger in this package — which means its
38
+ * children have to be plain visual content. Nesting a Button inside one gives
39
+ * the inner pressable the touch and the sheet never opens, silently. Same
40
+ * footgun the overlay triggers already carry.
41
+ */
42
+ function Trigger({ children, style }) {
43
+ const sheet = useSheet("Trigger");
44
+ return (_jsx(Pressable, { onPress: sheet.open, style: style, children: children }));
45
+ }
46
+ function Content({ children, style }) {
47
+ const theme = useTheme();
48
+ const state = useContext(SheetRefContext);
49
+ // Read here, in the normal tree, and handed back down below — see the note
50
+ // on the provider inside the modal.
51
+ const sheet = useContext(SheetContext);
52
+ if (!state || !sheet) {
53
+ throw new Error("Sheet.Content must be rendered inside Sheet.");
54
+ }
55
+ const { ref, defaultOpen, snapPoints, onOpenChange } = state;
56
+ useEffect(() => {
57
+ if (defaultOpen)
58
+ ref.current?.present();
59
+ }, [defaultOpen, ref]);
60
+ // The modal renders nothing until it is presented, so there is no invisible
61
+ // backdrop sitting over the screen eating taps while it is closed. That was
62
+ // a real hazard with the inline version and had to be handled by unmounting.
63
+ /**
64
+ * The sheet's own background, drawn here rather than left to `backgroundStyle`.
65
+ *
66
+ * Two things needed fixing and one prop could not do either.
67
+ *
68
+ * `backgroundStyle` paints a view sized exactly to the sheet, and the
69
+ * container `style` sits outside the rounded corners — so a border on the
70
+ * container drew a straight line across the full width above the rounded top,
71
+ * which is the stray line that was reported.
72
+ *
73
+ * And the spring overshoots. A sheet sized exactly to its snap point travels
74
+ * past it and leaves a band of backdrop along the bottom of the screen for a
75
+ * frame or two. So this hangs `grytDrawerBleed` below the bottom edge, which
76
+ * is the same trick and the same distance the web Drawer uses — its comment
77
+ * calls the artefact "a seam of backdrop down the edge".
78
+ */
79
+ const renderBackground = useCallback(({ style: bgStyle }) => (_jsx(View, { pointerEvents: "none", style: [
80
+ bgStyle,
81
+ {
82
+ backgroundColor: theme.color.surface,
83
+ borderTopLeftRadius: theme.radius.xl,
84
+ borderTopRightRadius: theme.radius.xl,
85
+ // Follows the rounded corners, unlike a border on the container.
86
+ borderTopWidth: 1,
87
+ borderColor: theme.color.border,
88
+ // The overhang. Negative bottom rather than extra height, so the
89
+ // rounded top stays where the sheet actually is.
90
+ bottom: -grytDrawerBleed,
91
+ },
92
+ ] })), [theme]);
93
+ /**
94
+ * Gryt's spring, rather than gorhom's default.
95
+ *
96
+ * `withTiming` over the sampled curve, for the same reason every other
97
+ * animation in this package does it that way: the curve in @gryt/theme is a
98
+ * damped spring solved analytically, and approximating it with a physics
99
+ * engine would be approximating an exact curve with the thing it was chosen
100
+ * over.
101
+ *
102
+ * `springSoft` — 700ms — because a sheet travels its own height, which is the
103
+ * case that duration was shaped for. It is what Drawer uses and for the same
104
+ * reason.
105
+ */
106
+ const animationConfigs = useBottomSheetTimingConfigs({
107
+ duration: durations.springSoft,
108
+ easing: easeSpring,
109
+ });
110
+ const renderBackdrop = useCallback((props) => (_jsx(BottomSheetBackdrop, { ...props, appearsOnIndex: 0, disappearsOnIndex: -1,
111
+ // Tracks the drag, so dragging the sheet down lightens the scrim
112
+ // rather than holding full opacity until it snaps shut.
113
+ opacity: 0.6, pressBehavior: "close" })), []);
114
+ return (_jsx(BottomSheetModal, { ref: ref, snapPoints: snapPoints, enablePanDownToClose: true, onDismiss: () => onOpenChange?.(false), animationConfigs: animationConfigs, backdropComponent: renderBackdrop, backgroundComponent: renderBackground, handleIndicatorStyle: {
115
+ backgroundColor: theme.color.border,
116
+ width: 36,
117
+ height: 4,
118
+ }, children: _jsx(BottomSheetView, { style: [{ flex: 1, padding: theme.space(4) }, style], children: _jsx(SheetContext.Provider, { value: sheet, children: children }) }) }));
119
+ }
120
+ function Close({ children, style }) {
121
+ const sheet = useSheet("Close");
122
+ return (_jsx(Pressable, { onPress: sheet.close, style: style, children: children }));
123
+ }
124
+ function Title({ children, style }) {
125
+ const theme = useTheme();
126
+ return (_jsx(View, { style: style, children: _jsx(Text, { accessibilityRole: "header", style: {
127
+ color: theme.color.text,
128
+ fontSize: 18,
129
+ fontWeight: "600",
130
+ marginBottom: theme.space(2),
131
+ }, children: children }) }));
132
+ }
133
+ export const Sheet = Object.assign(Root, { Trigger, Content, Close, Title });
@@ -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;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
+ {"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;AAIrE,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,+BAuKb"}