@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.
- 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.map +1 -1
- package/dist/components/Drawer/Drawer.js +28 -3
- 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 +90 -0
- package/dist/components/Sheet/Sheet.d.ts.map +1 -0
- package/dist/components/Sheet/Sheet.js +133 -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/package.json +9 -5
|
@@ -5,6 +5,7 @@ import Animated, { useAnimatedStyle, useSharedValue } from "react-native-reanima
|
|
|
5
5
|
import { grytScaleSteps } from "@gryt/theme";
|
|
6
6
|
import { springy } from "../../motion/index.js";
|
|
7
7
|
import { toneRamp, useTheme } from "../../theme/index.js";
|
|
8
|
+
import { useDragLock } from "../internal/dragLock.js";
|
|
8
9
|
import { valueAt } from "./sliderValue.js";
|
|
9
10
|
const TRACK_HEIGHT = 4;
|
|
10
11
|
const THUMB = 20;
|
|
@@ -20,6 +21,9 @@ const THUMB = 20;
|
|
|
20
21
|
*/
|
|
21
22
|
export function Slider({ value: controlled, defaultValue = 0, onValueChange, onValueCommit, min = 0, max = 100, step = 1, disabled, tone = "primary", style, accessibilityLabel, }) {
|
|
22
23
|
const theme = useTheme();
|
|
24
|
+
// Held for the duration of a drag, so the list this is sitting in stays put.
|
|
25
|
+
// A no-op outside a ScrollView from this package — see useDragLock.
|
|
26
|
+
const dragLock = useDragLock();
|
|
23
27
|
const [uncontrolled, setUncontrolled] = useState(defaultValue);
|
|
24
28
|
const value = controlled ?? uncontrolled;
|
|
25
29
|
const [width, setWidth] = useState(0);
|
|
@@ -31,6 +35,9 @@ export function Slider({ value: controlled, defaultValue = 0, onValueChange, onV
|
|
|
31
35
|
// fire from touch events, which is always after an effect has run.
|
|
32
36
|
const state = useRef({ width, value, min, max, step, disabled });
|
|
33
37
|
const emit = useRef({ onValueChange, onValueCommit, controlled });
|
|
38
|
+
// Same reason as the others: the responder callbacks are created once, and
|
|
39
|
+
// the lock's identity changes whenever the count does.
|
|
40
|
+
const lockRef = useRef(dragLock);
|
|
34
41
|
/** Where the current gesture started, in track coordinates. */
|
|
35
42
|
const origin = useRef(0);
|
|
36
43
|
/**
|
|
@@ -47,6 +54,9 @@ export function Slider({ value: controlled, defaultValue = 0, onValueChange, onV
|
|
|
47
54
|
useEffect(() => {
|
|
48
55
|
emit.current = { onValueChange, onValueCommit, controlled };
|
|
49
56
|
}, [onValueChange, onValueCommit, controlled]);
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
lockRef.current = dragLock;
|
|
59
|
+
}, [dragLock]);
|
|
50
60
|
const valueFromX = (x) => {
|
|
51
61
|
const s = state.current;
|
|
52
62
|
// Before layout there is no position to read, so hold what we have rather
|
|
@@ -76,6 +86,7 @@ export function Slider({ value: controlled, defaultValue = 0, onValueChange, onV
|
|
|
76
86
|
// here once it is clearly horizontal.
|
|
77
87
|
onMoveShouldSetPanResponder: () => !state.current.disabled,
|
|
78
88
|
onPanResponderGrant: (e) => {
|
|
89
|
+
lockRef.current.lock();
|
|
79
90
|
// Where the finger landed, kept for the drag to offset from.
|
|
80
91
|
origin.current = e.nativeEvent.locationX;
|
|
81
92
|
thumbScale.value = springy(grytScaleSteps.sliderThumb.press);
|
|
@@ -94,7 +105,29 @@ export function Slider({ value: controlled, defaultValue = 0, onValueChange, onV
|
|
|
94
105
|
onPanResponderMove: (_e, gesture) => {
|
|
95
106
|
apply(valueFromX(origin.current + gesture.dx));
|
|
96
107
|
},
|
|
108
|
+
// The fix for GRYT-390's "the slider still lets me scroll the page".
|
|
109
|
+
//
|
|
110
|
+
// The comment above says this "claims the gesture and holds it". It did
|
|
111
|
+
// not: `onPanResponderTerminationRequest` defaults to *granting*, so the
|
|
112
|
+
// enclosing ScrollView asked for the responder as soon as the finger
|
|
113
|
+
// moved and got it. Every drag inside a scrolling screen scrolled the
|
|
114
|
+
// screen, which is every drag — volume sliders live in settings lists.
|
|
115
|
+
//
|
|
116
|
+
// Saying no is the whole fix. `onShouldBlockNativeResponder` stops the
|
|
117
|
+
// native scroll view taking over underneath on Android, where the JS
|
|
118
|
+
// answer alone is not enough.
|
|
119
|
+
onPanResponderTerminationRequest: () => false,
|
|
120
|
+
// A refused termination can still happen — the system can take the
|
|
121
|
+
// responder away regardless, on a call or a notification. Without this
|
|
122
|
+
// the lock leaks and the screen never scrolls again, which is a far
|
|
123
|
+
// worse bug than the one being fixed.
|
|
124
|
+
onPanResponderTerminate: () => {
|
|
125
|
+
lockRef.current.unlock();
|
|
126
|
+
thumbScale.value = springy(1);
|
|
127
|
+
},
|
|
128
|
+
onShouldBlockNativeResponder: () => true,
|
|
97
129
|
onPanResponderRelease: () => {
|
|
130
|
+
lockRef.current.unlock();
|
|
98
131
|
thumbScale.value = springy(1);
|
|
99
132
|
emit.current.onValueCommit?.(state.current.value);
|
|
100
133
|
},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
1
2
|
import { type StyleProp, type ViewStyle } from "react-native";
|
|
2
3
|
export type SwitchTone = "primary" | "secondary" | "neutral" | "danger";
|
|
3
4
|
export interface SwitchProps {
|
|
@@ -6,6 +7,8 @@ export interface SwitchProps {
|
|
|
6
7
|
onCheckedChange?: (checked: boolean) => void;
|
|
7
8
|
disabled?: boolean;
|
|
8
9
|
tone?: SwitchTone;
|
|
10
|
+
/** Tapping this toggles the switch, as a `<label>` does on the web. */
|
|
11
|
+
label?: ReactNode;
|
|
9
12
|
style?: StyleProp<ViewStyle>;
|
|
10
13
|
accessibilityLabel?: string;
|
|
11
14
|
}
|
|
@@ -21,5 +24,5 @@ export interface SwitchProps {
|
|
|
21
24
|
* The cost is that it does not pick up future platform restyling for free, which
|
|
22
25
|
* is the trade the whole library is already making.
|
|
23
26
|
*/
|
|
24
|
-
export declare function Switch({ checked: controlled, defaultChecked, onCheckedChange, disabled, tone, style, accessibilityLabel, }: SwitchProps): import("react").JSX.Element;
|
|
27
|
+
export declare function Switch({ checked: controlled, defaultChecked, onCheckedChange, disabled, tone, label, style, accessibilityLabel, }: SwitchProps): import("react").JSX.Element;
|
|
25
28
|
//# sourceMappingURL=Switch.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Switch.d.ts","sourceRoot":"","sources":["../../../src/components/Switch/Switch.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Switch.d.ts","sourceRoot":"","sources":["../../../src/components/Switch/Switch.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC5D,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAS9D,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC;AAExE,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IAC7C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,uEAAuE;IACvE,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAQD;;;;;;;;;;;GAWG;AACH,wBAAgB,MAAM,CAAC,EACrB,OAAO,EAAE,UAAU,EACnB,cAAsB,EACtB,eAAe,EACf,QAAQ,EACR,IAAgB,EAChB,KAAK,EACL,KAAK,EACL,kBAAkB,GACnB,EAAE,WAAW,+BAsEb"}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
|
-
import { Pressable } from "react-native";
|
|
4
3
|
import Animated, { useAnimatedStyle, useSharedValue } from "react-native-reanimated";
|
|
5
4
|
import { useReducedMotion } from "../../hooks/useReducedMotion.js";
|
|
6
5
|
import { springy } from "../../motion/index.js";
|
|
6
|
+
import { grytScaleSteps } from "@gryt/theme";
|
|
7
7
|
import { toneRamp, useTheme } from "../../theme/index.js";
|
|
8
|
+
import { ControlRow } from "../internal/ControlRow.js";
|
|
8
9
|
// The web is h-6 w-10 with a thumb translating x-4. Same numbers in points.
|
|
9
10
|
const TRACK_WIDTH = 40;
|
|
10
11
|
const TRACK_HEIGHT = 24;
|
|
@@ -22,7 +23,7 @@ const TRAVEL = 16;
|
|
|
22
23
|
* The cost is that it does not pick up future platform restyling for free, which
|
|
23
24
|
* is the trade the whole library is already making.
|
|
24
25
|
*/
|
|
25
|
-
export function Switch({ checked: controlled, defaultChecked = false, onCheckedChange, disabled, tone = "primary", style, accessibilityLabel, }) {
|
|
26
|
+
export function Switch({ checked: controlled, defaultChecked = false, onCheckedChange, disabled, tone = "primary", label, style, accessibilityLabel, }) {
|
|
26
27
|
const theme = useTheme();
|
|
27
28
|
const reducedMotion = useReducedMotion();
|
|
28
29
|
const [uncontrolled, setUncontrolled] = useState(defaultChecked);
|
|
@@ -41,29 +42,32 @@ export function Switch({ checked: controlled, defaultChecked = false, onCheckedC
|
|
|
41
42
|
transform: [{ translateX: offset.value }]
|
|
42
43
|
}));
|
|
43
44
|
const ramp = toneRamp(theme, tone);
|
|
44
|
-
return (_jsx(
|
|
45
|
+
return (_jsx(ControlRow, { label: label, onPress: () => {
|
|
45
46
|
const next = !checked;
|
|
46
47
|
if (controlled === undefined)
|
|
47
48
|
setUncontrolled(next);
|
|
48
49
|
onCheckedChange?.(next);
|
|
49
|
-
}, style:
|
|
50
|
-
{
|
|
50
|
+
}, disabled: disabled, pressScale: grytScaleSteps.switch.press, accessibilityRole: "switch", accessibilityState: { checked, disabled: !!disabled }, accessibilityLabel: accessibilityLabel, style: style, children: _jsx(Animated.View, { style: {
|
|
51
51
|
width: TRACK_WIDTH,
|
|
52
52
|
height: TRACK_HEIGHT,
|
|
53
53
|
borderRadius: TRACK_HEIGHT / 2,
|
|
54
54
|
padding: (TRACK_HEIGHT - THUMB) / 2,
|
|
55
55
|
justifyContent: "center",
|
|
56
|
-
|
|
56
|
+
// The web outlines the track and fills it with surfaceRaised when
|
|
57
|
+
// off, then paints the tone over it and drops the border to
|
|
58
|
+
// transparent. This was neutral[5] with no border, which is close
|
|
59
|
+
// enough to look right on its own and wrong beside a checkbox.
|
|
60
|
+
borderWidth: 1,
|
|
61
|
+
borderColor: checked ? "transparent" : theme.color.border,
|
|
62
|
+
backgroundColor: checked ? ramp[8] : theme.color.surfaceRaised,
|
|
57
63
|
opacity: disabled ? 0.5 : 1,
|
|
58
|
-
},
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
thumbStyle,
|
|
68
|
-
] }) }));
|
|
64
|
+
}, children: _jsx(Animated.View, { style: [
|
|
65
|
+
{
|
|
66
|
+
width: THUMB,
|
|
67
|
+
height: THUMB,
|
|
68
|
+
borderRadius: THUMB / 2,
|
|
69
|
+
backgroundColor: checked ? theme.color.bg : theme.color.text,
|
|
70
|
+
},
|
|
71
|
+
thumbStyle,
|
|
72
|
+
] }) }) }));
|
|
69
73
|
}
|
|
@@ -1,20 +1,49 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import { type StyleProp, type ViewStyle } from "react-native";
|
|
3
3
|
type TabValue = string | number;
|
|
4
|
+
export type TabsOrientation = "horizontal" | "vertical";
|
|
4
5
|
export interface TabsProps {
|
|
5
6
|
value?: TabValue;
|
|
6
7
|
defaultValue?: TabValue;
|
|
7
8
|
onValueChange?: (value: TabValue) => void;
|
|
9
|
+
/**
|
|
10
|
+
* A rail rather than a row. Same visual language turned ninety degrees, and
|
|
11
|
+
* the same caveat the web carries: past about a dozen destinations a filled
|
|
12
|
+
* pill becomes a block of accent parked in the corner, and something quieter
|
|
13
|
+
* is the better call.
|
|
14
|
+
*/
|
|
15
|
+
orientation?: TabsOrientation;
|
|
8
16
|
children?: ReactNode;
|
|
9
17
|
style?: StyleProp<ViewStyle>;
|
|
10
18
|
}
|
|
11
|
-
declare function Root({ value: controlled, defaultValue, onValueChange, children, style, }: TabsProps): import("react").JSX.Element;
|
|
19
|
+
declare function Root({ value: controlled, defaultValue, onValueChange, orientation, children, style, }: TabsProps): import("react").JSX.Element;
|
|
12
20
|
export interface TabsListProps {
|
|
13
21
|
children?: ReactNode;
|
|
14
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Tabs that do not fit scroll sideways rather than overflowing.
|
|
24
|
+
*
|
|
25
|
+
* Off by default, which is the web's behaviour and the reason this defaults
|
|
26
|
+
* the way it does — the web has no scroller here at all. It is kept because
|
|
27
|
+
* a phone is narrow and a five-tab rail genuinely does not fit, and clipping
|
|
28
|
+
* silently is worse than scrolling. Horizontal only; a vertical rail is a
|
|
29
|
+
* column in a screen that already scrolls.
|
|
30
|
+
*/
|
|
15
31
|
scrollable?: boolean;
|
|
16
32
|
style?: StyleProp<ViewStyle>;
|
|
17
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* The pill rail, and the indicator that slides across it.
|
|
36
|
+
*
|
|
37
|
+
* The indicator lives here rather than in `Tabs.Indicator` because it needs
|
|
38
|
+
* every tab's measured box, and the list is the only part that sees them all.
|
|
39
|
+
* `Tabs.Indicator` is still exported and still renders nothing, so a call site
|
|
40
|
+
* copied from the web keeps working.
|
|
41
|
+
*
|
|
42
|
+
* The previous version of this file drew a 2px underline per tab and said
|
|
43
|
+
* sliding one pill was "real work for a decoration". It is real work, and the
|
|
44
|
+
* decoration is most of what the component looks like — an underline and a
|
|
45
|
+
* filled pill are not the same design with different paint.
|
|
46
|
+
*/
|
|
18
47
|
declare function List({ children, scrollable, style }: TabsListProps): import("react").JSX.Element;
|
|
19
48
|
export interface TabProps {
|
|
20
49
|
value: TabValue;
|
|
@@ -22,16 +51,6 @@ export interface TabProps {
|
|
|
22
51
|
disabled?: boolean;
|
|
23
52
|
style?: StyleProp<ViewStyle>;
|
|
24
53
|
}
|
|
25
|
-
/**
|
|
26
|
-
* The underline is drawn by the tab, not by a separate indicator.
|
|
27
|
-
*
|
|
28
|
-
* `@gryt/ui` has a `Tabs.Indicator` that Base UI slides between tabs, measured
|
|
29
|
-
* against the active one. Sliding it here means measuring every tab and
|
|
30
|
-
* animating between them, which is real work for a decoration. Each tab draws
|
|
31
|
-
* its own bottom border instead: it appears rather than slides.
|
|
32
|
-
*
|
|
33
|
-
* `Indicator` is still exported, as a no-op, so call sites match.
|
|
34
|
-
*/
|
|
35
54
|
declare function Tab({ value, children, disabled, style }: TabProps): import("react").JSX.Element;
|
|
36
55
|
export interface TabsPanelProps {
|
|
37
56
|
value: TabValue;
|
|
@@ -39,7 +58,13 @@ export interface TabsPanelProps {
|
|
|
39
58
|
style?: StyleProp<ViewStyle>;
|
|
40
59
|
}
|
|
41
60
|
declare function Panel({ value, children, style }: TabsPanelProps): import("react").JSX.Element | null;
|
|
42
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* Kept so a call site copied from the web keeps working.
|
|
63
|
+
*
|
|
64
|
+
* The indicator is drawn by `List`, which is the only part that has measured
|
|
65
|
+
* every tab. Rendering it here would mean measuring the list from inside one of
|
|
66
|
+
* its children.
|
|
67
|
+
*/
|
|
43
68
|
declare function Indicator(): null;
|
|
44
69
|
export declare const Tabs: typeof Root & {
|
|
45
70
|
List: typeof List;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Tabs.d.ts","sourceRoot":"","sources":["../../../src/components/Tabs/Tabs.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"Tabs.d.ts","sourceRoot":"","sources":["../../../src/components/Tabs/Tabs.tsx"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,EAML,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAOtB,KAAK,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;AAChC,MAAM,MAAM,eAAe,GAAG,YAAY,GAAG,UAAU,CAAC;AA4BxD,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,YAAY,CAAC,EAAE,QAAQ,CAAC;IACxB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAC;IAC1C;;;;;OAKG;IACH,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED,iBAAS,IAAI,CAAC,EACZ,KAAK,EAAE,UAAU,EACjB,YAAY,EACZ,aAAa,EACb,WAA0B,EAC1B,QAAQ,EACR,KAAK,GACN,EAAE,SAAS,+BA4DX;AAID,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED;;;;;;;;;;;;GAYG;AACH,iBAAS,IAAI,CAAC,EAAE,QAAQ,EAAE,UAAkB,EAAE,KAAK,EAAE,EAAE,aAAa,+BAmFnE;AAED,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,QAAQ,CAAC;IAChB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED,iBAAS,GAAG,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,QAAQ,+BA2D1D;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,QAAQ,CAAC;IAChB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED,iBAAS,KAAK,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,cAAc,sCAsBxD;AAED;;;;;;GAMG;AACH,iBAAS,SAAS,IAAI,IAAI,CAEzB;AAED,eAAO,MAAM,IAAI;;;;;CAAuD,CAAC;AACzE,OAAO,EAAE,GAAG,EAAE,CAAC"}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { createContext, useContext, useState } from "react";
|
|
3
|
-
import { Pressable, ScrollView, Text, View } from "react-native";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useCallback, useContext, useMemo, useState, } from "react";
|
|
3
|
+
import { Pressable, ScrollView, Text, View, } from "react-native";
|
|
4
|
+
import Animated, { useAnimatedStyle, useSharedValue } from "react-native-reanimated";
|
|
5
|
+
import { useReducedMotion } from "../../hooks/useReducedMotion.js";
|
|
6
|
+
import { springy } from "../../motion/index.js";
|
|
4
7
|
import { useTheme } from "../../theme/index.js";
|
|
5
8
|
const TabsContext = createContext(null);
|
|
6
9
|
function useTabs(part) {
|
|
@@ -9,72 +12,184 @@ function useTabs(part) {
|
|
|
9
12
|
throw new Error(`Tabs.${part} must be rendered inside Tabs.`);
|
|
10
13
|
return value;
|
|
11
14
|
}
|
|
12
|
-
|
|
15
|
+
/** Matches the web: `p-1` on the list, so the pill insets by 4 on every side. */
|
|
16
|
+
const LIST_PADDING = 4;
|
|
17
|
+
function Root({ value: controlled, defaultValue, onValueChange, orientation = "horizontal", children, style, }) {
|
|
13
18
|
const [uncontrolled, setUncontrolled] = useState(defaultValue ?? null);
|
|
19
|
+
const [boxes, setBoxes] = useState({});
|
|
14
20
|
const value = controlled ?? uncontrolled;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
// Keyed by String(value) because a Record cannot key on `string | number`
|
|
22
|
+
// and 1 and "1" would collide. They are already distinct TabValues, so
|
|
23
|
+
// collapsing them here would make two tabs share one measurement.
|
|
24
|
+
const report = useCallback((tab, box) => {
|
|
25
|
+
setBoxes((prev) => {
|
|
26
|
+
const key = `${typeof tab}:${tab}`;
|
|
27
|
+
const seen = prev[key];
|
|
28
|
+
if (seen &&
|
|
29
|
+
seen.x === box.x &&
|
|
30
|
+
seen.y === box.y &&
|
|
31
|
+
seen.width === box.width &&
|
|
32
|
+
seen.height === box.height) {
|
|
33
|
+
// onLayout fires on every re-render in some trees, and setting state
|
|
34
|
+
// unconditionally from it is an infinite loop.
|
|
35
|
+
return prev;
|
|
36
|
+
}
|
|
37
|
+
return { ...prev, [key]: box };
|
|
38
|
+
});
|
|
39
|
+
}, []);
|
|
40
|
+
const context = useMemo(() => ({
|
|
41
|
+
value,
|
|
42
|
+
orientation,
|
|
43
|
+
report,
|
|
44
|
+
setValue: (next) => {
|
|
45
|
+
if (controlled === undefined)
|
|
46
|
+
setUncontrolled(next);
|
|
47
|
+
onValueChange?.(next);
|
|
48
|
+
},
|
|
49
|
+
}), [value, orientation, report, controlled, onValueChange]);
|
|
50
|
+
return (_jsx(TabsContext.Provider, { value: context, children: _jsx(BoxesContext.Provider, { value: boxes, children: _jsx(View, { style: [
|
|
51
|
+
// Vertical puts the rail and the panel side by side, as the web's
|
|
52
|
+
// `data-[orientation=vertical]:flex` does.
|
|
53
|
+
orientation === "vertical"
|
|
54
|
+
? { flexDirection: "row", alignItems: "stretch" }
|
|
55
|
+
: null,
|
|
56
|
+
style,
|
|
57
|
+
], children: children }) }) }));
|
|
23
58
|
}
|
|
24
|
-
|
|
59
|
+
const BoxesContext = createContext({});
|
|
60
|
+
/**
|
|
61
|
+
* The pill rail, and the indicator that slides across it.
|
|
62
|
+
*
|
|
63
|
+
* The indicator lives here rather than in `Tabs.Indicator` because it needs
|
|
64
|
+
* every tab's measured box, and the list is the only part that sees them all.
|
|
65
|
+
* `Tabs.Indicator` is still exported and still renders nothing, so a call site
|
|
66
|
+
* copied from the web keeps working.
|
|
67
|
+
*
|
|
68
|
+
* The previous version of this file drew a 2px underline per tab and said
|
|
69
|
+
* sliding one pill was "real work for a decoration". It is real work, and the
|
|
70
|
+
* decoration is most of what the component looks like — an underline and a
|
|
71
|
+
* filled pill are not the same design with different paint.
|
|
72
|
+
*/
|
|
73
|
+
function List({ children, scrollable = false, style }) {
|
|
25
74
|
const theme = useTheme();
|
|
26
|
-
const
|
|
75
|
+
const tabs = useTabs("List");
|
|
76
|
+
const boxes = useContext(BoxesContext);
|
|
77
|
+
const reducedMotion = useReducedMotion();
|
|
78
|
+
const vertical = tabs.orientation === "vertical";
|
|
79
|
+
const active = tabs.value === null ? undefined : boxes[`${typeof tabs.value}:${tabs.value}`];
|
|
80
|
+
// Starts at 0 and is only ever animated to a measured box, so the pill grows
|
|
81
|
+
// out of the left edge on first paint. The web has the same first-frame
|
|
82
|
+
// problem and solves it with `renderBeforeHydration`; here the first layout
|
|
83
|
+
// arrives in the same frame as the first paint, so there is nothing to see.
|
|
84
|
+
const x = useSharedValue(0);
|
|
85
|
+
const y = useSharedValue(0);
|
|
86
|
+
const w = useSharedValue(0);
|
|
87
|
+
const h = useSharedValue(0);
|
|
88
|
+
if (active) {
|
|
89
|
+
const to = reducedMotion
|
|
90
|
+
? { x: active.x, y: active.y, w: active.width, h: active.height }
|
|
91
|
+
: null;
|
|
92
|
+
/* eslint-disable react-hooks/immutability */
|
|
93
|
+
// Assigned during render on purpose: a shared value is not React state and
|
|
94
|
+
// does not schedule one, and doing it in an effect would land the animation
|
|
95
|
+
// a frame after the tab's colour has already changed.
|
|
96
|
+
x.value = to ? to.x : springy(active.x);
|
|
97
|
+
y.value = to ? to.y : springy(active.y);
|
|
98
|
+
w.value = to ? to.w : springy(active.width);
|
|
99
|
+
h.value = to ? to.h : springy(active.height);
|
|
100
|
+
/* eslint-enable react-hooks/immutability */
|
|
101
|
+
}
|
|
102
|
+
const indicatorStyle = useAnimatedStyle(() => ({
|
|
103
|
+
transform: [{ translateX: x.value }, { translateY: y.value }],
|
|
104
|
+
width: w.value,
|
|
105
|
+
height: h.value,
|
|
106
|
+
}));
|
|
107
|
+
const row = (_jsxs(View, { accessibilityRole: "tablist", style: [
|
|
27
108
|
{
|
|
28
|
-
flexDirection: "row",
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
109
|
+
flexDirection: vertical ? "column" : "row",
|
|
110
|
+
alignItems: vertical ? "stretch" : "center",
|
|
111
|
+
alignSelf: vertical ? "auto" : "flex-start",
|
|
112
|
+
gap: LIST_PADDING,
|
|
113
|
+
padding: LIST_PADDING,
|
|
114
|
+
backgroundColor: theme.color.surfaceRaised,
|
|
115
|
+
// A 999px radius on a tall box bows its short edges, which is why the
|
|
116
|
+
// web drops the rail to `lg` rather than keeping the pill shape.
|
|
117
|
+
borderRadius: vertical ? theme.radius.lg : theme.radius.full,
|
|
32
118
|
},
|
|
33
119
|
style,
|
|
34
|
-
], children:
|
|
35
|
-
|
|
120
|
+
], children: [active ? (_jsx(Animated.View, { pointerEvents: "none", style: [
|
|
121
|
+
{
|
|
122
|
+
position: "absolute",
|
|
123
|
+
left: 0,
|
|
124
|
+
top: 0,
|
|
125
|
+
borderRadius: vertical ? theme.radius.md : theme.radius.full,
|
|
126
|
+
backgroundColor: theme.color.accent,
|
|
127
|
+
},
|
|
128
|
+
indicatorStyle,
|
|
129
|
+
] })) : null, children] }));
|
|
130
|
+
if (!scrollable || vertical)
|
|
36
131
|
return row;
|
|
37
132
|
return (_jsx(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, children: row }));
|
|
38
133
|
}
|
|
39
|
-
/**
|
|
40
|
-
* The underline is drawn by the tab, not by a separate indicator.
|
|
41
|
-
*
|
|
42
|
-
* `@gryt/ui` has a `Tabs.Indicator` that Base UI slides between tabs, measured
|
|
43
|
-
* against the active one. Sliding it here means measuring every tab and
|
|
44
|
-
* animating between them, which is real work for a decoration. Each tab draws
|
|
45
|
-
* its own bottom border instead: it appears rather than slides.
|
|
46
|
-
*
|
|
47
|
-
* `Indicator` is still exported, as a no-op, so call sites match.
|
|
48
|
-
*/
|
|
49
134
|
function Tab({ value, children, disabled, style }) {
|
|
50
135
|
const theme = useTheme();
|
|
51
136
|
const tabs = useTabs("Tab");
|
|
52
137
|
const active = tabs.value === value;
|
|
53
|
-
|
|
138
|
+
const vertical = tabs.orientation === "vertical";
|
|
139
|
+
const onLayout = useCallback((e) => {
|
|
140
|
+
const { x, y, width, height } = e.nativeEvent.layout;
|
|
141
|
+
tabs.report(value, { x, y, width, height });
|
|
142
|
+
}, [tabs, value]);
|
|
143
|
+
return (_jsx(Pressable, { accessibilityRole: "tab", accessibilityState: { selected: active, disabled: !!disabled }, disabled: disabled, onPress: () => tabs.setValue(value), onLayout: onLayout,
|
|
144
|
+
// Sits above the indicator, which is absolutely positioned behind it.
|
|
145
|
+
// Without this the pill paints over its own label.
|
|
146
|
+
style: [
|
|
54
147
|
{
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
148
|
+
zIndex: 1,
|
|
149
|
+
minHeight: vertical ? 36 : 32,
|
|
150
|
+
paddingHorizontal: vertical ? 12 : 16,
|
|
151
|
+
paddingVertical: 6,
|
|
152
|
+
alignItems: "center",
|
|
153
|
+
justifyContent: vertical ? "flex-start" : "center",
|
|
154
|
+
flexDirection: "row",
|
|
155
|
+
gap: vertical ? 10 : 0,
|
|
156
|
+
borderRadius: vertical ? theme.radius.md : theme.radius.full,
|
|
59
157
|
opacity: disabled ? 0.5 : 1,
|
|
60
158
|
},
|
|
61
159
|
style,
|
|
62
|
-
], children: typeof children === "string" ? (_jsx(Text, { style: {
|
|
63
|
-
|
|
160
|
+
], children: typeof children === "string" ? (_jsx(Text, { numberOfLines: 1, style: {
|
|
161
|
+
// The tab only changes its text colour; the fill belongs to the
|
|
162
|
+
// indicator so it can travel. `onAccent` rather than `text`,
|
|
163
|
+
// because the pill underneath is the accent.
|
|
164
|
+
color: active ? theme.color.onAccent : theme.color.muted,
|
|
64
165
|
fontSize: 14,
|
|
65
|
-
|
|
166
|
+
lineHeight: 20,
|
|
167
|
+
fontWeight: "500",
|
|
66
168
|
}, children: children })) : (children) }));
|
|
67
169
|
}
|
|
68
170
|
function Panel({ value, children, style }) {
|
|
171
|
+
const theme = useTheme();
|
|
69
172
|
const tabs = useTabs("Panel");
|
|
70
173
|
if (tabs.value !== value)
|
|
71
174
|
return null;
|
|
175
|
+
const vertical = tabs.orientation === "vertical";
|
|
72
176
|
// React Native has "tab" and "tablist" but no "tabpanel" role, so there is
|
|
73
|
-
// nothing to put here. The panel is still reachable, it just does not
|
|
74
|
-
// itself as the tab's panel. In the exceptions table.
|
|
75
|
-
return _jsx(View, { style:
|
|
177
|
+
// nothing to put here. The panel is still reachable, it just does not
|
|
178
|
+
// announce itself as the tab's panel. In the exceptions table.
|
|
179
|
+
return (_jsx(View, { style: [
|
|
180
|
+
vertical
|
|
181
|
+
? { flex: 1, minWidth: 0, paddingLeft: theme.space(4) }
|
|
182
|
+
: { paddingTop: theme.space(3) },
|
|
183
|
+
style,
|
|
184
|
+
], children: children }));
|
|
76
185
|
}
|
|
77
|
-
/**
|
|
186
|
+
/**
|
|
187
|
+
* Kept so a call site copied from the web keeps working.
|
|
188
|
+
*
|
|
189
|
+
* The indicator is drawn by `List`, which is the only part that has measured
|
|
190
|
+
* every tab. Rendering it here would mean measuring the list from inside one of
|
|
191
|
+
* its children.
|
|
192
|
+
*/
|
|
78
193
|
function Indicator() {
|
|
79
194
|
return null;
|
|
80
195
|
}
|
|
@@ -14,6 +14,5 @@ export interface ToggleProps {
|
|
|
14
14
|
style?: StyleProp<ViewStyle>;
|
|
15
15
|
accessibilityLabel?: string;
|
|
16
16
|
}
|
|
17
|
-
/** A button that stays down. Mute, deafen, and the rest of the call controls. */
|
|
18
17
|
export declare function Toggle({ pressed: controlled, defaultPressed, onPressedChange, disabled, tone, size, children, style, accessibilityLabel, }: ToggleProps): import("react").JSX.Element;
|
|
19
18
|
//# sourceMappingURL=Toggle.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Toggle.d.ts","sourceRoot":"","sources":["../../../src/components/Toggle/Toggle.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACjD,OAAO,EAAmB,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"Toggle.d.ts","sourceRoot":"","sources":["../../../src/components/Toggle/Toggle.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACjD,OAAO,EAAmB,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAM/E,OAAO,EAAsB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAErE,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,aAAa,EAAE,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC,CAAC;AAChG,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AASjE,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IAC7C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAaD,wBAAgB,MAAM,CAAC,EACrB,OAAO,EAAE,UAAU,EACnB,cAAsB,EACtB,eAAe,EACf,QAAQ,EACR,IAAgB,EAChB,IAAe,EACf,QAAQ,EACR,KAAK,EACL,kBAAkB,GACnB,EAAE,WAAW,+BAoDb"}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from "react";
|
|
3
3
|
import { Pressable, Text } from "react-native";
|
|
4
|
+
import Animated from "react-native-reanimated";
|
|
5
|
+
import { grytScaleSteps } from "@gryt/theme";
|
|
6
|
+
import { usePressScale } from "../../motion/index.js";
|
|
4
7
|
import { toneRamp, useTheme } from "../../theme/index.js";
|
|
5
8
|
const SIZES = {
|
|
6
9
|
xsmall: { minHeight: 32, paddingH: 10, fontSize: 12 },
|
|
@@ -9,18 +12,29 @@ const SIZES = {
|
|
|
9
12
|
large: { minHeight: 48, paddingH: 18, fontSize: 16 },
|
|
10
13
|
};
|
|
11
14
|
/** A button that stays down. Mute, deafen, and the rest of the call controls. */
|
|
15
|
+
/**
|
|
16
|
+
* Animated.createAnimatedComponent rather than a wrapping Animated.View.
|
|
17
|
+
*
|
|
18
|
+
* A Toggle is laid out by its parent — a Toolbar puts them in a row — and an
|
|
19
|
+
* extra view between the two would take the layout props and leave the button
|
|
20
|
+
* sized by its content. The web scales the button element itself, so this does
|
|
21
|
+
* too.
|
|
22
|
+
*/
|
|
23
|
+
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
|
|
12
24
|
export function Toggle({ pressed: controlled, defaultPressed = false, onPressedChange, disabled, tone = "primary", size = "medium", children, style, accessibilityLabel, }) {
|
|
13
25
|
const theme = useTheme();
|
|
26
|
+
const press = usePressScale(grytScaleSteps.toggle.press, disabled);
|
|
14
27
|
const [uncontrolled, setUncontrolled] = useState(defaultPressed);
|
|
15
28
|
const on = controlled ?? uncontrolled;
|
|
16
29
|
const metrics = SIZES[size];
|
|
17
30
|
const ramp = toneRamp(theme, tone);
|
|
18
|
-
return (_jsx(
|
|
31
|
+
return (_jsx(AnimatedPressable, { accessibilityRole: "button", accessibilityState: { selected: on, disabled: !!disabled }, accessibilityLabel: accessibilityLabel, disabled: disabled, onPressIn: press.onPressIn, onPressOut: press.onPressOut, onPress: () => {
|
|
19
32
|
const next = !on;
|
|
20
33
|
if (controlled === undefined)
|
|
21
34
|
setUncontrolled(next);
|
|
22
35
|
onPressedChange?.(next);
|
|
23
36
|
}, style: [
|
|
37
|
+
press.style,
|
|
24
38
|
{
|
|
25
39
|
minHeight: metrics.minHeight,
|
|
26
40
|
paddingHorizontal: metrics.paddingH,
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { type StyleProp, type ViewStyle } from "react-native";
|
|
3
|
+
/**
|
|
4
|
+
* A control and its label, where tapping either one works.
|
|
5
|
+
*
|
|
6
|
+
* The web gets this for free: the sign-in screen wraps `<Checkbox>` in a
|
|
7
|
+
* `<label>` and the browser forwards the click. React Native has no `<label>`
|
|
8
|
+
* and no `htmlFor`, so the association has to be structural — the label and the
|
|
9
|
+
* control are inside one Pressable, and that Pressable is the target.
|
|
10
|
+
*
|
|
11
|
+
* This is a parity *match* reached through a different API, which is why the
|
|
12
|
+
* exceptions table lists it as an API difference rather than a behaviour one.
|
|
13
|
+
* Tapping a label toggles the control on both platforms; only the way you say
|
|
14
|
+
* so differs.
|
|
15
|
+
*
|
|
16
|
+
* Without a label it renders the control alone, so the bare form still works
|
|
17
|
+
* and nothing existing has to change.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* How far past its own edge a control still counts as pressed.
|
|
21
|
+
*
|
|
22
|
+
* There is no web equivalent and nothing to be 1:1 with — a pointer is exact
|
|
23
|
+
* and a fingertip is about 9mm across. 20×20 is well under the 44pt minimum
|
|
24
|
+
* both Apple and WCAG ask for, and radios are usually stacked, so a near miss
|
|
25
|
+
* lands on the neighbour rather than on nothing.
|
|
26
|
+
*
|
|
27
|
+
* 12 on each side takes a 20pt box to 44pt without moving a single pixel of
|
|
28
|
+
* what is drawn. It is deliberately not padding: padding would change the
|
|
29
|
+
* layout and the spacing between a control and its label.
|
|
30
|
+
*/
|
|
31
|
+
export declare const CONTROL_HIT_SLOP: {
|
|
32
|
+
top: number;
|
|
33
|
+
bottom: number;
|
|
34
|
+
left: number;
|
|
35
|
+
right: number;
|
|
36
|
+
};
|
|
37
|
+
export interface ControlRowProps {
|
|
38
|
+
/** Tapping this toggles the control. Omit for a bare control. */
|
|
39
|
+
label?: ReactNode;
|
|
40
|
+
children: ReactNode;
|
|
41
|
+
onPress: () => void;
|
|
42
|
+
disabled?: boolean;
|
|
43
|
+
/** From `grytScaleSteps`, so it matches whatever the web does. */
|
|
44
|
+
pressScale: number;
|
|
45
|
+
accessibilityRole: "checkbox" | "radio" | "switch";
|
|
46
|
+
accessibilityState: {
|
|
47
|
+
checked?: boolean | "mixed";
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
};
|
|
50
|
+
accessibilityLabel?: string;
|
|
51
|
+
style?: StyleProp<ViewStyle>;
|
|
52
|
+
}
|
|
53
|
+
export declare function ControlRow({ label, children, onPress, disabled, pressScale, accessibilityRole, accessibilityState, accessibilityLabel, style, }: ControlRowProps): import("react").JSX.Element;
|
|
54
|
+
//# sourceMappingURL=ControlRow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ControlRow.d.ts","sourceRoot":"","sources":["../../../src/components/internal/ControlRow.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,EAAyB,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAMrF;;;;;;;;;;;;;;;GAeG;AAEH;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gBAAgB;;;;;CAA+C,CAAC;AAE7E,MAAM,WAAW,eAAe;IAC9B,iEAAiE;IACjE,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,SAAS,CAAC;IACpB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,kEAAkE;IAClE,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,UAAU,GAAG,OAAO,GAAG,QAAQ,CAAC;IACnD,kBAAkB,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IACxE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,UAAU,EACV,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,KAAK,GACN,EAAE,eAAe,+BAqDjB"}
|