@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
@@ -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":"AACA,OAAO,EAAa,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAOzE,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC;AAExE,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IAC7C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAQD;;;;;;;;;;;GAWG;AACH,wBAAgB,MAAM,CAAC,EACrB,OAAO,EAAE,UAAU,EACnB,cAAsB,EACtB,eAAe,EACf,QAAQ,EACR,IAAgB,EAChB,KAAK,EACL,kBAAkB,GACnB,EAAE,WAAW,+BA6Db"}
1
+ {"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(Pressable, { accessibilityRole: "switch", accessibilityState: { checked, disabled: !!disabled }, accessibilityLabel: accessibilityLabel, disabled: disabled, onPress: () => {
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
- backgroundColor: checked ? ramp[8] : theme.scales.neutral[5],
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
- style,
60
- ], children: _jsx(Animated.View, { style: [
61
- {
62
- width: THUMB,
63
- height: THUMB,
64
- borderRadius: THUMB / 2,
65
- backgroundColor: checked ? theme.color.bg : theme.color.text,
66
- },
67
- thumbStyle,
68
- ] }) }));
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
- /** Tabs that do not fit scroll sideways rather than wrapping or shrinking. */
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
- /** See Tab: the underline belongs to the tab here. Kept so call sites match. */
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,EAAuC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC5E,OAAO,EAAqC,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAIjG,KAAK,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;AAehC,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,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,QAAQ,EACR,KAAK,GACN,EAAE,SAAS,+BAgBX;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED,iBAAS,IAAI,CAAC,EAAE,QAAQ,EAAE,UAAiB,EAAE,KAAK,EAAE,EAAE,aAAa,+BAyBlE;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;;;;;;;;;GASG;AACH,iBAAS,GAAG,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,QAAQ,+BAqC1D;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,sCAOxD;AAED,gFAAgF;AAChF,iBAAS,SAAS,IAAI,IAAI,CAEzB;AAED,eAAO,MAAM,IAAI;;;;;CAAuD,CAAC;AACzE,OAAO,EAAE,GAAG,EAAE,CAAC"}
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
- function Root({ value: controlled, defaultValue, onValueChange, children, style, }) {
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
- return (_jsx(TabsContext.Provider, { value: {
16
- value,
17
- setValue: (next) => {
18
- if (controlled === undefined)
19
- setUncontrolled(next);
20
- onValueChange?.(next);
21
- },
22
- }, children: _jsx(View, { style: style, children: children }) }));
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
- function List({ children, scrollable = true, style }) {
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 row = (_jsx(View, { accessibilityRole: "tablist", style: [
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
- gap: theme.space(1),
30
- borderBottomWidth: 1,
31
- borderBottomColor: theme.color.border,
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: children }));
35
- if (!scrollable)
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
- return (_jsx(Pressable, { accessibilityRole: "tab", accessibilityState: { selected: active, disabled: !!disabled }, disabled: disabled, onPress: () => tabs.setValue(value), style: [
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
- paddingHorizontal: theme.space(4),
56
- paddingVertical: theme.space(3),
57
- borderBottomWidth: 2,
58
- borderBottomColor: active ? theme.color.accent : "transparent",
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
- color: active ? theme.color.text : theme.color.muted,
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
- fontWeight: active ? "600" : "500",
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 announce
74
- // itself as the tab's panel. In the exceptions table.
75
- return _jsx(View, { style: style, children: children });
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
- /** See Tab: the underline belongs to the tab here. Kept so call sites match. */
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;AAE/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;AAED,iFAAiF;AACjF,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,+BAgDb"}
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(Pressable, { accessibilityRole: "button", accessibilityState: { selected: on, disabled: !!disabled }, accessibilityLabel: accessibilityLabel, disabled: disabled, onPress: () => {
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"}