@gryt/ui-native 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/dist/components/AlertDialog/AlertDialog.d.ts.map +1 -1
- package/dist/components/Checkbox/Checkbox.d.ts +7 -1
- package/dist/components/Checkbox/Checkbox.d.ts.map +1 -1
- package/dist/components/Checkbox/Checkbox.js +47 -15
- package/dist/components/Dialog/Dialog.d.ts.map +1 -1
- package/dist/components/Dialog/Dialog.js +10 -1
- package/dist/components/Drawer/Drawer.d.ts +0 -7
- package/dist/components/Drawer/Drawer.d.ts.map +1 -1
- package/dist/components/Drawer/Drawer.js +229 -30
- package/dist/components/Progress/Progress.d.ts +11 -17
- package/dist/components/Progress/Progress.d.ts.map +1 -1
- package/dist/components/Progress/Progress.js +66 -22
- package/dist/components/Radio/Radio.d.ts +9 -1
- package/dist/components/Radio/Radio.d.ts.map +1 -1
- package/dist/components/Radio/Radio.js +38 -14
- package/dist/components/ScrollArea/ScrollArea.d.ts +6 -4
- package/dist/components/ScrollArea/ScrollArea.d.ts.map +1 -1
- package/dist/components/ScrollArea/ScrollArea.js +19 -4
- package/dist/components/Sheet/Sheet.d.ts +99 -0
- package/dist/components/Sheet/Sheet.d.ts.map +1 -0
- package/dist/components/Sheet/Sheet.js +201 -0
- package/dist/components/Slider/Slider.d.ts.map +1 -1
- package/dist/components/Slider/Slider.js +33 -0
- package/dist/components/Switch/Switch.d.ts +4 -1
- package/dist/components/Switch/Switch.d.ts.map +1 -1
- package/dist/components/Switch/Switch.js +21 -17
- package/dist/components/Tabs/Tabs.d.ts +38 -13
- package/dist/components/Tabs/Tabs.d.ts.map +1 -1
- package/dist/components/Tabs/Tabs.js +157 -42
- package/dist/components/Toggle/Toggle.d.ts +0 -1
- package/dist/components/Toggle/Toggle.d.ts.map +1 -1
- package/dist/components/Toggle/Toggle.js +15 -1
- package/dist/components/internal/ControlRow.d.ts +54 -0
- package/dist/components/internal/ControlRow.d.ts.map +1 -0
- package/dist/components/internal/ControlRow.js +59 -0
- package/dist/components/internal/dragLock.d.ts +43 -0
- package/dist/components/internal/dragLock.d.ts.map +1 -0
- package/dist/components/internal/dragLock.js +18 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/motion/motion.d.ts +13 -3
- package/dist/motion/motion.d.ts.map +1 -1
- package/dist/motion/motion.js +6 -6
- package/package.json +11 -5
|
@@ -1,30 +1,62 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { View, } from "react-native";
|
|
4
|
+
import Animated, { Easing, useAnimatedStyle, useSharedValue, withRepeat, withTiming, } from "react-native-reanimated";
|
|
5
|
+
import { grytDurations } from "@gryt/theme";
|
|
6
|
+
import { useReducedMotion } from "../../hooks/useReducedMotion.js";
|
|
3
7
|
import { useTheme } from "../../theme/index.js";
|
|
8
|
+
/** How much of the track the sweeping bar covers. Matches the web's `width: 40%`. */
|
|
9
|
+
const SWEEP_FRACTION = 0.4;
|
|
4
10
|
/**
|
|
5
|
-
* Determinate
|
|
11
|
+
* Determinate travel follows the web deliberately: 300ms on ease-out rather
|
|
12
|
+
* than the spring. A progress bar is a reading, and the spring's 12% overshoot
|
|
13
|
+
* on a jump to 100% runs past the end of the track and comes back — which reads
|
|
14
|
+
* as the job finishing, unfinishing, then finishing.
|
|
6
15
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* So both platforms agree, and both are missing the feature. Implementing it
|
|
16
|
-
* belongs on both at once — GRYT-382 — rather than here, where it would make
|
|
17
|
-
* React Native the one that behaves differently.
|
|
18
|
-
*
|
|
19
|
-
* The travel that *is* implemented follows the web deliberately: 300ms on
|
|
20
|
-
* ease-out rather than the spring. A progress bar is a reading, and the
|
|
21
|
-
* spring's 12% overshoot on a jump to 100% runs past the end of the track and
|
|
22
|
-
* comes back — which reads as the job finishing, unfinishing, then finishing.
|
|
16
|
+
* Indeterminate is a partial bar sweeping the track on a loop, at
|
|
17
|
+
* `grytDurations.sweep`, from the same description the web's keyframe follows.
|
|
18
|
+
* Both were empty until GRYT-382: the web passed `value={null}` to Base UI,
|
|
19
|
+
* which leaves the indicator's width unset, and nothing styled it. This file
|
|
20
|
+
* previously called that a parity exception, which it was not — both platforms
|
|
21
|
+
* were missing the same feature, and writing it down as one-sided is what
|
|
22
|
+
* closed the question for a while.
|
|
23
23
|
*/
|
|
24
24
|
export function Progress({ value, tone = "accent", style }) {
|
|
25
25
|
const theme = useTheme();
|
|
26
|
-
const
|
|
27
|
-
|
|
26
|
+
const reducedMotion = useReducedMotion();
|
|
27
|
+
const indeterminate = value === undefined;
|
|
28
|
+
const clamped = indeterminate ? 0 : Math.max(0, Math.min(100, value));
|
|
29
|
+
// Measured rather than expressed as a percentage. The web animates
|
|
30
|
+
// `translateX` in percentages of the bar's own width, which React Native does
|
|
31
|
+
// not accept in a transform, so the same travel is computed in points from
|
|
32
|
+
// the track's own layout.
|
|
33
|
+
const [trackWidth, setTrackWidth] = useState(0);
|
|
34
|
+
const offset = useSharedValue(0);
|
|
35
|
+
const barWidth = trackWidth * SWEEP_FRACTION;
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!indeterminate || reducedMotion || trackWidth === 0)
|
|
38
|
+
return;
|
|
39
|
+
// Starts fully off the left edge and ends fully off the right. Leaving the
|
|
40
|
+
// track completely at both ends is what makes the loop seam invisible — a
|
|
41
|
+
// bar that restarts while still on screen flicks backwards once per pass.
|
|
42
|
+
// eslint-disable-next-line react-hooks/immutability
|
|
43
|
+
offset.value = -barWidth;
|
|
44
|
+
// eslint-disable-next-line react-hooks/immutability
|
|
45
|
+
offset.value = withRepeat(withTiming(trackWidth, {
|
|
46
|
+
duration: grytDurations.sweep,
|
|
47
|
+
easing: Easing.inOut(Easing.ease),
|
|
48
|
+
}), -1, false);
|
|
49
|
+
}, [indeterminate, reducedMotion, trackWidth, barWidth, offset]);
|
|
50
|
+
const sweepStyle = useAnimatedStyle(() => ({
|
|
51
|
+
transform: [{ translateX: offset.value }],
|
|
52
|
+
}));
|
|
53
|
+
const onLayout = (e) => {
|
|
54
|
+
const next = e.nativeEvent.layout.width;
|
|
55
|
+
// Guarded because onLayout fires on every re-render in some trees, and
|
|
56
|
+
// setting state unconditionally from it is an infinite loop.
|
|
57
|
+
setTrackWidth((prev) => (prev === next ? prev : next));
|
|
58
|
+
};
|
|
59
|
+
return (_jsx(View, { accessibilityRole: "progressbar", accessibilityValue: indeterminate ? undefined : { now: clamped, min: 0, max: 100 }, onLayout: indeterminate ? onLayout : undefined, style: [
|
|
28
60
|
{
|
|
29
61
|
height: 6,
|
|
30
62
|
borderRadius: theme.radius.full,
|
|
@@ -32,10 +64,22 @@ export function Progress({ value, tone = "accent", style }) {
|
|
|
32
64
|
overflow: "hidden",
|
|
33
65
|
},
|
|
34
66
|
style,
|
|
35
|
-
], children: _jsx(View, { style:
|
|
67
|
+
], children: indeterminate ? (_jsx(Animated.View, { style: [
|
|
68
|
+
{
|
|
69
|
+
// Full width and dimmed under reduce-motion rather than a frozen
|
|
70
|
+
// partial bar, which reads as a job that stalled 40% in. Matches
|
|
71
|
+
// what the web's `prefers-reduced-motion` rule does.
|
|
72
|
+
width: reducedMotion ? "100%" : barWidth,
|
|
73
|
+
opacity: reducedMotion ? 0.4 : 1,
|
|
74
|
+
height: "100%",
|
|
75
|
+
borderRadius: theme.radius.full,
|
|
76
|
+
backgroundColor: theme.scales[tone][8],
|
|
77
|
+
},
|
|
78
|
+
reducedMotion ? null : sweepStyle,
|
|
79
|
+
] })) : (_jsx(View, { style: {
|
|
36
80
|
width: `${clamped}%`,
|
|
37
81
|
height: "100%",
|
|
38
82
|
borderRadius: theme.radius.full,
|
|
39
83
|
backgroundColor: theme.scales[tone][8],
|
|
40
|
-
} }) }));
|
|
84
|
+
} })) }));
|
|
41
85
|
}
|
|
@@ -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,
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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,99 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type StyleProp, type ViewStyle } from "react-native";
|
|
3
|
+
import { type OpenStateProps } from "../../overlay/useOpenState.js";
|
|
4
|
+
/**
|
|
5
|
+
* A bottom sheet, which is the phone's modal.
|
|
6
|
+
*
|
|
7
|
+
* `@gryt/ui` has no counterpart, and that is deliberate rather than an
|
|
8
|
+
* oversight to be corrected later. A sheet is what a phone does where the web
|
|
9
|
+
* opens a dialog or slides a drawer, and the two are not the same interaction:
|
|
10
|
+
* a sheet is dragged, it settles at heights the user chooses, and dismissing it
|
|
11
|
+
* is a flick rather than a click on an X. Shipping the web's Dialog on a phone
|
|
12
|
+
* would be 1:1 and wrong; inventing a sheet on the web would be worse. So this
|
|
13
|
+
* is an addition, and the README lists it as one.
|
|
14
|
+
*
|
|
15
|
+
* Built on `@gorhom/bottom-sheet` rather than by hand, which is the opposite of
|
|
16
|
+
* the call made for Slider and Tabs, and the reason is the shape of the
|
|
17
|
+
* problem. Those were shallow and Gryt-specific — a press scale is four lines
|
|
18
|
+
* and no library will give you the right one. A sheet is deep and generic:
|
|
19
|
+
* velocity-based dismiss, snap points, keyboard avoidance, a backdrop whose
|
|
20
|
+
* opacity tracks the drag, and the handoff between dragging the sheet and
|
|
21
|
+
* scrolling the content inside it. None of that is about Gryt and all of it is
|
|
22
|
+
* months of edge cases.
|
|
23
|
+
*
|
|
24
|
+
* The library supplies behaviour only. Every colour, radius and dimension below
|
|
25
|
+
* comes from the theme.
|
|
26
|
+
*
|
|
27
|
+
* **`SheetProvider` has to be mounted above anything that renders a Sheet**,
|
|
28
|
+
* next to `ToastProvider` and `TooltipProvider`. That is not ceremony: this is
|
|
29
|
+
* `BottomSheetModal`, not `BottomSheet`, and the difference is where the sheet
|
|
30
|
+
* is anchored.
|
|
31
|
+
*
|
|
32
|
+
* `BottomSheet` positions itself inside whatever container it is rendered in.
|
|
33
|
+
* Written inline in a screen, as this was first, it anchors to its slot in the
|
|
34
|
+
* layout — so it appeared partway down the page with the content behind it
|
|
35
|
+
* showing through, and read as entering from the top. `BottomSheetModal`
|
|
36
|
+
* portals to the provider instead, which sits at the app root, so it is always
|
|
37
|
+
* anchored to the bottom of the screen no matter where the call site is.
|
|
38
|
+
*/
|
|
39
|
+
export interface SheetProviderProps {
|
|
40
|
+
children?: ReactNode;
|
|
41
|
+
}
|
|
42
|
+
/** Mount once, at the root. See the note above for why it is required. */
|
|
43
|
+
export declare function SheetProvider({ children }: SheetProviderProps): import("react").JSX.Element;
|
|
44
|
+
export interface SheetProps extends OpenStateProps {
|
|
45
|
+
/**
|
|
46
|
+
* Heights the sheet settles at, as percentages or points.
|
|
47
|
+
*
|
|
48
|
+
* The web has no equivalent concept — a dialog is one size — which is part of
|
|
49
|
+
* why this is its own component rather than a Drawer with a prop.
|
|
50
|
+
*/
|
|
51
|
+
snapPoints?: (string | number)[];
|
|
52
|
+
children?: ReactNode;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* `open` with `onOpenChange` drives it from outside; `defaultOpen` alone lets
|
|
56
|
+
* it manage itself. That is `useOpenState`, which every other overlay in this
|
|
57
|
+
* package already uses — Sheet was the one that did not, and took `defaultOpen`
|
|
58
|
+
* and a `Trigger` and nothing else.
|
|
59
|
+
*
|
|
60
|
+
* What that cost a caller: a sheet opened by something that is not a Pressable
|
|
61
|
+
* — a tab bar item, a notification, a deep link — had to be unmounted and
|
|
62
|
+
* remounted with `defaultOpen` to open it at all, which throws the body away on
|
|
63
|
+
* every open. The mobile app shell did exactly that, twice.
|
|
64
|
+
*/
|
|
65
|
+
declare function Root({ snapPoints, children, ...openProps }: SheetProps): import("react").JSX.Element;
|
|
66
|
+
export interface SheetTriggerProps {
|
|
67
|
+
children?: ReactNode;
|
|
68
|
+
style?: StyleProp<ViewStyle>;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* A Pressable, like every other trigger in this package — which means its
|
|
72
|
+
* children have to be plain visual content. Nesting a Button inside one gives
|
|
73
|
+
* the inner pressable the touch and the sheet never opens, silently. Same
|
|
74
|
+
* footgun the overlay triggers already carry.
|
|
75
|
+
*/
|
|
76
|
+
declare function Trigger({ children, style }: SheetTriggerProps): import("react").JSX.Element;
|
|
77
|
+
export interface SheetContentProps {
|
|
78
|
+
children?: ReactNode;
|
|
79
|
+
style?: StyleProp<ViewStyle>;
|
|
80
|
+
}
|
|
81
|
+
declare function Content({ children, style }: SheetContentProps): import("react").JSX.Element;
|
|
82
|
+
export interface SheetCloseProps {
|
|
83
|
+
children?: ReactNode;
|
|
84
|
+
style?: StyleProp<ViewStyle>;
|
|
85
|
+
}
|
|
86
|
+
declare function Close({ children, style }: SheetCloseProps): import("react").JSX.Element;
|
|
87
|
+
export interface SheetTitleProps {
|
|
88
|
+
children?: ReactNode;
|
|
89
|
+
style?: StyleProp<ViewStyle>;
|
|
90
|
+
}
|
|
91
|
+
declare function Title({ children, style }: SheetTitleProps): import("react").JSX.Element;
|
|
92
|
+
export declare const Sheet: typeof Root & {
|
|
93
|
+
Trigger: typeof Trigger;
|
|
94
|
+
Content: typeof Content;
|
|
95
|
+
Close: typeof Close;
|
|
96
|
+
Title: typeof Title;
|
|
97
|
+
};
|
|
98
|
+
export {};
|
|
99
|
+
//# 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,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAG/E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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,UAAW,SAAQ,cAAc;IAChD;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IACjC,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED;;;;;;;;;;GAUG;AACH,iBAAS,IAAI,CAAC,EACZ,UAAoB,EACpB,QAAQ,EACR,GAAG,SAAS,EACb,EAAE,UAAU,+BAqBZ;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,+BA2LtD;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,201 @@
|
|
|
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 { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
6
|
+
import { grytDrawerBleed } from "@gryt/theme";
|
|
7
|
+
import { durations, easeSpring } from "../../motion/index.js";
|
|
8
|
+
import { useOpenState } from "../../overlay/useOpenState.js";
|
|
9
|
+
import { useTheme } from "../../theme/index.js";
|
|
10
|
+
/** Mount once, at the root. See the note above for why it is required. */
|
|
11
|
+
export function SheetProvider({ children }) {
|
|
12
|
+
return _jsx(BottomSheetModalProvider, { children: children });
|
|
13
|
+
}
|
|
14
|
+
const SheetContext = createContext(null);
|
|
15
|
+
function useSheet(part) {
|
|
16
|
+
const value = useContext(SheetContext);
|
|
17
|
+
if (!value)
|
|
18
|
+
throw new Error(`Sheet.${part} must be rendered inside Sheet.`);
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* `open` with `onOpenChange` drives it from outside; `defaultOpen` alone lets
|
|
23
|
+
* it manage itself. That is `useOpenState`, which every other overlay in this
|
|
24
|
+
* package already uses — Sheet was the one that did not, and took `defaultOpen`
|
|
25
|
+
* and a `Trigger` and nothing else.
|
|
26
|
+
*
|
|
27
|
+
* What that cost a caller: a sheet opened by something that is not a Pressable
|
|
28
|
+
* — a tab bar item, a notification, a deep link — had to be unmounted and
|
|
29
|
+
* remounted with `defaultOpen` to open it at all, which throws the body away on
|
|
30
|
+
* every open. The mobile app shell did exactly that, twice.
|
|
31
|
+
*/
|
|
32
|
+
function Root({ snapPoints = ["50%"], children, ...openProps }) {
|
|
33
|
+
const ref = useRef(null);
|
|
34
|
+
const state = useOpenState(openProps);
|
|
35
|
+
const { open: isOpen, setOpen } = state;
|
|
36
|
+
// Asking for the state rather than reaching for the ref, so a controlled
|
|
37
|
+
// Sheet's parent gets the final say — `setOpen` on a controlled overlay
|
|
38
|
+
// reports and does not decide. The effect in `Content` is what actually
|
|
39
|
+
// presents, once the modal it presents exists.
|
|
40
|
+
const open = useCallback(() => setOpen(true), [setOpen]);
|
|
41
|
+
const close = useCallback(() => setOpen(false), [setOpen]);
|
|
42
|
+
const context = useMemo(() => ({ open, close }), [open, close]);
|
|
43
|
+
return (_jsx(SheetContext.Provider, { value: context, children: _jsx(SheetRefContext.Provider, { value: { ref, isOpen, setOpen, snapPoints }, children: children }) }));
|
|
44
|
+
}
|
|
45
|
+
const SheetRefContext = createContext(null);
|
|
46
|
+
/**
|
|
47
|
+
* A Pressable, like every other trigger in this package — which means its
|
|
48
|
+
* children have to be plain visual content. Nesting a Button inside one gives
|
|
49
|
+
* the inner pressable the touch and the sheet never opens, silently. Same
|
|
50
|
+
* footgun the overlay triggers already carry.
|
|
51
|
+
*/
|
|
52
|
+
function Trigger({ children, style }) {
|
|
53
|
+
const sheet = useSheet("Trigger");
|
|
54
|
+
return (_jsx(Pressable, { onPress: sheet.open, style: style, children: children }));
|
|
55
|
+
}
|
|
56
|
+
function Content({ children, style }) {
|
|
57
|
+
const theme = useTheme();
|
|
58
|
+
/**
|
|
59
|
+
* The phone's own furniture, which the sheet has to stay clear of at both
|
|
60
|
+
* ends.
|
|
61
|
+
*
|
|
62
|
+
* At the tall snap point the sheet reaches the top of the screen, and without
|
|
63
|
+
* `topInset` its content runs under the Dynamic Island. At the other end the
|
|
64
|
+
* home indicator sits over the last few points, and a control row ending
|
|
65
|
+
* flush with the sheet is clipped by it.
|
|
66
|
+
*/
|
|
67
|
+
const insets = useSafeAreaInsets();
|
|
68
|
+
const state = useContext(SheetRefContext);
|
|
69
|
+
// Read here, in the normal tree, and handed back down below — see the note
|
|
70
|
+
// on the provider inside the modal.
|
|
71
|
+
const sheet = useContext(SheetContext);
|
|
72
|
+
if (!state || !sheet) {
|
|
73
|
+
throw new Error("Sheet.Content must be rendered inside Sheet.");
|
|
74
|
+
}
|
|
75
|
+
const { ref, isOpen, setOpen, snapPoints } = state;
|
|
76
|
+
/**
|
|
77
|
+
* `present` and `dismiss` rather than mounting and unmounting by hand: the
|
|
78
|
+
* modal is always rendered and the provider decides whether it is on screen,
|
|
79
|
+
* which is what removes the mount-then-snap-on-the-next-frame dance the
|
|
80
|
+
* inline version needed.
|
|
81
|
+
*
|
|
82
|
+
* Here rather than in `Root` because `ref` is attached to the modal below,
|
|
83
|
+
* and `Root` runs before there is one.
|
|
84
|
+
*
|
|
85
|
+
* `presented` is not bookkeeping that could be dropped. A closed Sheet runs
|
|
86
|
+
* this effect once on mount with `isOpen` false, and dismissing a modal that
|
|
87
|
+
* has never been presented takes it *out* of the provider's registry — after
|
|
88
|
+
* which `present()` is a no-op and the sheet never opens again. It looks like
|
|
89
|
+
* the open prop being ignored, and it is not: it is the close that ran first.
|
|
90
|
+
*/
|
|
91
|
+
const presented = useRef(false);
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (isOpen) {
|
|
94
|
+
presented.current = true;
|
|
95
|
+
ref.current?.present();
|
|
96
|
+
}
|
|
97
|
+
else if (presented.current) {
|
|
98
|
+
presented.current = false;
|
|
99
|
+
ref.current?.dismiss();
|
|
100
|
+
}
|
|
101
|
+
}, [isOpen, ref]);
|
|
102
|
+
// The modal renders nothing until it is presented, so there is no invisible
|
|
103
|
+
// backdrop sitting over the screen eating taps while it is closed. That was
|
|
104
|
+
// a real hazard with the inline version and had to be handled by unmounting.
|
|
105
|
+
/**
|
|
106
|
+
* The sheet's own background, drawn here rather than left to `backgroundStyle`.
|
|
107
|
+
*
|
|
108
|
+
* Two things needed fixing and one prop could not do either.
|
|
109
|
+
*
|
|
110
|
+
* `backgroundStyle` paints a view sized exactly to the sheet, and the
|
|
111
|
+
* container `style` sits outside the rounded corners — so a border on the
|
|
112
|
+
* container drew a straight line across the full width above the rounded top,
|
|
113
|
+
* which is the stray line that was reported.
|
|
114
|
+
*
|
|
115
|
+
* And the spring overshoots. A sheet sized exactly to its snap point travels
|
|
116
|
+
* past it and leaves a band of backdrop along the bottom of the screen for a
|
|
117
|
+
* frame or two. So this hangs `grytDrawerBleed` below the bottom edge, which
|
|
118
|
+
* is the same trick and the same distance the web Drawer uses — its comment
|
|
119
|
+
* calls the artefact "a seam of backdrop down the edge".
|
|
120
|
+
*/
|
|
121
|
+
const renderBackground = useCallback(({ style: bgStyle }) => (_jsx(View, { pointerEvents: "none", style: [
|
|
122
|
+
bgStyle,
|
|
123
|
+
{
|
|
124
|
+
backgroundColor: theme.color.surface,
|
|
125
|
+
borderTopLeftRadius: theme.radius.xl,
|
|
126
|
+
borderTopRightRadius: theme.radius.xl,
|
|
127
|
+
// Follows the rounded corners, unlike a border on the container.
|
|
128
|
+
borderTopWidth: 1,
|
|
129
|
+
borderColor: theme.color.border,
|
|
130
|
+
// The overhang. Negative bottom rather than extra height, so the
|
|
131
|
+
// rounded top stays where the sheet actually is.
|
|
132
|
+
bottom: -grytDrawerBleed,
|
|
133
|
+
},
|
|
134
|
+
] })), [theme]);
|
|
135
|
+
/**
|
|
136
|
+
* Gryt's spring, rather than gorhom's default.
|
|
137
|
+
*
|
|
138
|
+
* `withTiming` over the sampled curve, for the same reason every other
|
|
139
|
+
* animation in this package does it that way: the curve in @gryt/theme is a
|
|
140
|
+
* damped spring solved analytically, and approximating it with a physics
|
|
141
|
+
* engine would be approximating an exact curve with the thing it was chosen
|
|
142
|
+
* over.
|
|
143
|
+
*
|
|
144
|
+
* `springSoft` — 700ms — because a sheet travels its own height, which is the
|
|
145
|
+
* case that duration was shaped for. It is what Drawer uses and for the same
|
|
146
|
+
* reason.
|
|
147
|
+
*/
|
|
148
|
+
const animationConfigs = useBottomSheetTimingConfigs({
|
|
149
|
+
duration: durations.springSoft,
|
|
150
|
+
easing: easeSpring,
|
|
151
|
+
});
|
|
152
|
+
const renderBackdrop = useCallback((props) => (_jsx(BottomSheetBackdrop, { ...props, appearsOnIndex: 0, disappearsOnIndex: -1,
|
|
153
|
+
// Tracks the drag, so dragging the sheet down lightens the scrim
|
|
154
|
+
// rather than holding full opacity until it snaps shut.
|
|
155
|
+
opacity: 0.6, pressBehavior: "close" })), []);
|
|
156
|
+
return (_jsx(BottomSheetModal, { ref: ref, snapPoints: snapPoints,
|
|
157
|
+
// So a sheet at 100% stops below the Dynamic Island rather than running
|
|
158
|
+
// its content under it.
|
|
159
|
+
topInset: insets.top,
|
|
160
|
+
// Off, because `snapPoints` is the whole point of this component.
|
|
161
|
+
//
|
|
162
|
+
// gorhom v5 defaults dynamic sizing on, which measures the content and
|
|
163
|
+
// sizes the sheet to it — overriding the snap points entirely. A sheet
|
|
164
|
+
// asked for 70% whose content had no intrinsic height collapsed to the
|
|
165
|
+
// height of its own footer, which looks like the snap points being
|
|
166
|
+
// ignored because they were.
|
|
167
|
+
enableDynamicSizing: false, enablePanDownToClose: true,
|
|
168
|
+
// A flick down or a tap on the backdrop closes it without anything in
|
|
169
|
+
// React asking, so the state has to be told. Uncontrolled, this is what
|
|
170
|
+
// makes the next `present` work; controlled, it is how the parent finds
|
|
171
|
+
// out its sheet is gone.
|
|
172
|
+
onDismiss: () => setOpen(false), animationConfigs: animationConfigs, backdropComponent: renderBackdrop, backgroundComponent: renderBackground, handleIndicatorStyle: {
|
|
173
|
+
backgroundColor: theme.color.border,
|
|
174
|
+
width: 36,
|
|
175
|
+
height: 4,
|
|
176
|
+
}, children: _jsx(BottomSheetView, { style: [
|
|
177
|
+
{
|
|
178
|
+
flex: 1,
|
|
179
|
+
padding: theme.space(4),
|
|
180
|
+
// The home indicator's strip, on top of whatever padding the caller
|
|
181
|
+
// asked for. Without it the last row of content is clipped by it —
|
|
182
|
+
// reported as the voice controls being cut off at the bottom.
|
|
183
|
+
paddingBottom: theme.space(4) + insets.bottom,
|
|
184
|
+
},
|
|
185
|
+
style,
|
|
186
|
+
], children: _jsx(SheetContext.Provider, { value: sheet, children: children }) }) }));
|
|
187
|
+
}
|
|
188
|
+
function Close({ children, style }) {
|
|
189
|
+
const sheet = useSheet("Close");
|
|
190
|
+
return (_jsx(Pressable, { onPress: sheet.close, style: style, children: children }));
|
|
191
|
+
}
|
|
192
|
+
function Title({ children, style }) {
|
|
193
|
+
const theme = useTheme();
|
|
194
|
+
return (_jsx(View, { style: style, children: _jsx(Text, { accessibilityRole: "header", style: {
|
|
195
|
+
color: theme.color.text,
|
|
196
|
+
fontSize: 18,
|
|
197
|
+
fontWeight: "600",
|
|
198
|
+
marginBottom: theme.space(2),
|
|
199
|
+
}, children: children }) }));
|
|
200
|
+
}
|
|
201
|
+
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;
|
|
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"}
|