@swan-io/lake 1.6.0 → 1.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swan-io/lake",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "engines": {
5
5
  "node": ">=14.0.0",
6
6
  "yarn": "^1.20.0"
@@ -26,8 +26,8 @@
26
26
  ],
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
- "@popperjs/core": "^2.11.6",
30
- "@swan-io/boxed": "^0.12.1",
29
+ "@popperjs/core": "^2.11.7",
30
+ "@swan-io/boxed": "^0.13.0",
31
31
  "@swan-io/chicane": "^1.3.4",
32
32
  "dayjs": "^1.11.7",
33
33
  "polished": "^4.2.2",
@@ -35,22 +35,21 @@
35
35
  "react": "^18.2.0",
36
36
  "react-atomic-state": "^1.2.7",
37
37
  "react-dom": "^18.2.0",
38
- "react-native-web": "^0.19.1",
38
+ "react-native-web": "^0.19.4",
39
39
  "react-popper": "^2.3.0",
40
40
  "react-ux-form": "^1.3.0",
41
41
  "rifm": "^0.12.1",
42
42
  "ts-dedent": "^2.2.0",
43
- "ts-pattern": "^4.2.1",
44
- "urql": "^3.0.4",
43
+ "ts-pattern": "^4.2.2",
44
+ "urql": "^4.0.0",
45
45
  "uuid": "^9.0.0"
46
46
  },
47
47
  "devDependencies": {
48
- "@storybook/react": "^6.5.16",
49
- "@types/react": "^18.0.28",
48
+ "@types/react": "^18.0.35",
50
49
  "@types/react-dom": "^18.0.11",
51
50
  "@types/react-native": "^0.71.5",
52
51
  "@types/uuid": "^9.0.1",
53
52
  "jsdom": "^21.1.1",
54
- "type-fest": "^3.6.1"
53
+ "type-fest": "^3.8.0"
55
54
  }
56
55
  }
@@ -1,7 +1,99 @@
1
1
  /// <reference types="react" />
2
- import { Image, ImageProps } from "react-native";
3
- import { Except } from "type-fest";
4
- export declare const AutoWidthImage: import("react").MemoExoticComponent<import("react").ForwardRefExoticComponent<Except<ImageProps, "source"> & {
2
+ import { Image } from "react-native";
3
+ export declare const AutoWidthImage: import("react").MemoExoticComponent<import("react").ForwardRefExoticComponent<{
4
+ style?: import("react-native").StyleProp<import("react-native").ImageStyle>;
5
+ role?: import("react-native").WebRole | undefined;
6
+ defaultSource?: any;
7
+ draggable?: boolean | undefined;
8
+ id?: string | undefined;
9
+ onLayout?: ((event: import("react-native").LayoutChangeEvent) => void) | undefined;
10
+ onError?: ((error: import("react-native").NativeSyntheticEvent<import("react-native").ImageErrorEventData>) => void) | undefined;
11
+ onLoad?: ((event: import("react-native").NativeSyntheticEvent<import("react-native").ImageLoadEventData>) => void) | undefined;
12
+ onLoadEnd?: (() => void) | undefined;
13
+ onLoadStart?: (() => void) | undefined;
14
+ progressiveRenderingEnabled?: boolean | undefined;
15
+ borderRadius?: number | undefined;
16
+ borderTopLeftRadius?: number | undefined;
17
+ borderTopRightRadius?: number | undefined;
18
+ borderBottomLeftRadius?: number | undefined;
19
+ borderBottomRightRadius?: number | undefined;
20
+ resizeMode?: import("react-native").ImageResizeMode | undefined;
21
+ resizeMethod?: "auto" | "resize" | "scale" | undefined;
22
+ loadingIndicatorSource?: import("react-native").ImageURISource | undefined;
23
+ testID?: string | undefined;
24
+ nativeID?: string | undefined;
25
+ alt?: string | undefined;
26
+ blurRadius?: number | undefined;
27
+ capInsets?: import("react-native").Insets | undefined;
28
+ onProgress?: ((event: import("react-native").NativeSyntheticEvent<import("react-native").ImageProgressEventDataIOS>) => void) | undefined;
29
+ onPartialLoad?: (() => void) | undefined;
30
+ fadeDuration?: number | undefined;
31
+ accessible?: boolean | undefined;
32
+ accessibilityActions?: readonly Readonly<{
33
+ name: string;
34
+ label?: string | undefined;
35
+ }>[] | undefined;
36
+ accessibilityLabel?: string | undefined;
37
+ 'aria-label'?: string | undefined;
38
+ accessibilityRole?: import("react-native").AccessibilityRole | undefined;
39
+ accessibilityState?: import("react-native").AccessibilityState | undefined;
40
+ 'aria-busy'?: boolean | undefined;
41
+ 'aria-checked'?: boolean | "mixed" | undefined;
42
+ 'aria-disabled'?: boolean | undefined;
43
+ 'aria-expanded'?: boolean | undefined;
44
+ 'aria-selected'?: boolean | undefined;
45
+ 'aria-labelledby'?: string | undefined;
46
+ accessibilityHint?: string | undefined;
47
+ accessibilityValue?: import("react-native").AccessibilityValue | undefined;
48
+ 'aria-valuemax'?: number | undefined;
49
+ 'aria-valuemin'?: number | undefined;
50
+ 'aria-valuenow'?: number | undefined;
51
+ 'aria-valuetext'?: string | undefined;
52
+ onAccessibilityAction?: ((event: import("react-native").AccessibilityActionEvent) => void) | undefined;
53
+ importantForAccessibility?: "auto" | "yes" | "no" | "no-hide-descendants" | undefined;
54
+ 'aria-hidden'?: boolean | undefined;
55
+ 'aria-live'?: "polite" | "assertive" | "off" | undefined;
56
+ 'aria-modal'?: boolean | undefined;
57
+ accessibilityLiveRegion?: "none" | "polite" | "assertive" | undefined;
58
+ accessibilityElementsHidden?: boolean | undefined;
59
+ accessibilityViewIsModal?: boolean | undefined;
60
+ onAccessibilityEscape?: (() => void) | undefined;
61
+ onAccessibilityTap?: (() => void) | undefined;
62
+ onMagicTap?: (() => void) | undefined;
63
+ accessibilityIgnoresInvertColors?: boolean | undefined;
64
+ tabIndex?: 0 | -1 | undefined;
65
+ "aria-activedescendant"?: string | undefined;
66
+ "aria-atomic"?: boolean | undefined;
67
+ "aria-autocomplete"?: string | undefined;
68
+ "aria-colcount"?: number | undefined;
69
+ "aria-colindex"?: number | undefined;
70
+ "aria-colspan"?: number | undefined;
71
+ "aria-controls"?: string | undefined;
72
+ "aria-current"?: boolean | "time" | "page" | "step" | "location" | "date" | undefined;
73
+ "aria-describedby"?: string | undefined;
74
+ "aria-details"?: string | undefined;
75
+ "aria-errormessage"?: string | undefined;
76
+ "aria-flowto"?: string | undefined;
77
+ "aria-haspopup"?: string | undefined;
78
+ "aria-invalid"?: boolean | undefined;
79
+ "aria-keyshortcuts"?: string | undefined;
80
+ "aria-level"?: number | undefined;
81
+ "aria-multiline"?: boolean | undefined;
82
+ "aria-multiselectable"?: boolean | undefined;
83
+ "aria-orientation"?: "horizontal" | "vertical" | undefined;
84
+ "aria-owns"?: string | undefined;
85
+ "aria-placeholder"?: string | undefined;
86
+ "aria-posinset"?: number | undefined;
87
+ "aria-pressed"?: boolean | undefined;
88
+ "aria-readonly"?: boolean | undefined;
89
+ "aria-required"?: boolean | undefined;
90
+ "aria-roledescription"?: string | undefined;
91
+ "aria-rowcount"?: number | undefined;
92
+ "aria-rowindex"?: number | undefined;
93
+ "aria-rowspan"?: number | undefined;
94
+ "aria-setsize"?: number | undefined;
95
+ "aria-sort"?: "none" | "ascending" | "descending" | "other" | undefined;
96
+ } & {
5
97
  ariaLabel?: string | undefined;
6
98
  maxWidth?: number | undefined;
7
99
  height: number;
@@ -1,13 +1,22 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Suspense, useEffect, useState } from "react";
3
- import { Pressable, ScrollView, StyleSheet, View } from "react-native";
2
+ import { Suspense, useEffect, useMemo, useRef, useState } from "react";
3
+ import { PanResponder, Pressable, ScrollView, StyleSheet, View } from "react-native";
4
4
  import { commonStyles } from "../constants/commonStyles";
5
- import { backgroundColor, radii, shadows, spacings } from "../constants/design";
5
+ import { backgroundColor, colors, radii, shadows, spacings } from "../constants/design";
6
6
  import { useBodyClassName } from "../hooks/useBodyClassName";
7
+ import { limitElastic } from "../utils/math";
7
8
  import { FocusTrap } from "./FocusTrap";
8
9
  import { LoadingView } from "./LoadingView";
9
10
  import { Portal } from "./Portal";
10
11
  import { TransitionView } from "./TransitionView";
12
+ const ELASTIC_LENGTH = 100; // the maximum value you can reach
13
+ const ELASTIC_STRENGTH = 0.008; // higher value, maximum value reached faster
14
+ const limitGrab = limitElastic({
15
+ elasticLength: ELASTIC_LENGTH,
16
+ elasticStrength: ELASTIC_STRENGTH,
17
+ });
18
+ const DELTA_Y_CLOSE_THRESHOLD = 100;
19
+ const SWIPE_CLOSE_VELOCITY = 0.5;
11
20
  const BACKGROUND_COLOR = "rgba(0, 0, 0, 0.6)";
12
21
  const styles = StyleSheet.create({
13
22
  fill: {
@@ -53,6 +62,18 @@ const styles = StyleSheet.create({
53
62
  animationDuration: "300ms",
54
63
  animationTimingFunction: "ease-in-out",
55
64
  },
65
+ container: {
66
+ ...StyleSheet.absoluteFillObject,
67
+ transitionDuration: "300ms",
68
+ transitionProperty: "transform",
69
+ },
70
+ bottomCache: {
71
+ position: "absolute",
72
+ bottom: -ELASTIC_LENGTH + 1,
73
+ width: "100%",
74
+ height: ELASTIC_LENGTH,
75
+ backgroundColor: backgroundColor.accented,
76
+ },
56
77
  modalContainer: {
57
78
  ...StyleSheet.absoluteFillObject,
58
79
  },
@@ -69,14 +90,26 @@ const styles = StyleSheet.create({
69
90
  borderTopRightRadius: radii[8],
70
91
  boxShadow: shadows.modal,
71
92
  alignSelf: "stretch",
72
- marginTop: spacings[32],
73
93
  },
74
94
  pressableOverlay: {
75
- ...StyleSheet.absoluteFillObject,
95
+ ...commonStyles.fill,
96
+ outlineWidth: 0,
97
+ // make focus indicator invisible on iOS (outline: none doesn't work)
98
+ opacity: 0,
99
+ },
100
+ grabContainer: {
101
+ paddingHorizontal: 128,
102
+ paddingVertical: spacings[12],
103
+ },
104
+ grabLine: {
105
+ backgroundColor: colors.gray[100],
106
+ height: 5,
107
+ borderRadius: radii[4],
76
108
  },
77
109
  });
78
110
  export const BottomPanel = ({ visible, onPressClose, children, returnFocus = true }) => {
79
111
  const [rootElement, setRootElement] = useState(() => undefined);
112
+ const container = useRef(null);
80
113
  useEffect(() => {
81
114
  const rootElement = document.createElement("div");
82
115
  document.body.append(rootElement);
@@ -86,9 +119,38 @@ export const BottomPanel = ({ visible, onPressClose, children, returnFocus = tru
86
119
  setRootElement(undefined);
87
120
  };
88
121
  }, []);
122
+ const panResponder = useMemo(() => PanResponder.create({
123
+ onMoveShouldSetPanResponder: () => true,
124
+ onPanResponderGrant: () => {
125
+ if (container.current instanceof HTMLElement) {
126
+ container.current.style.transitionDuration = "0ms";
127
+ }
128
+ },
129
+ onPanResponderMove: (_event, { dy }) => {
130
+ const translateY = dy > 0 ? dy : -limitGrab(-dy);
131
+ if (container.current instanceof HTMLElement) {
132
+ container.current.style.transform = `translateY(${translateY}px)`;
133
+ }
134
+ },
135
+ onPanResponderRelease: (_event, gestureState) => {
136
+ if (container.current instanceof HTMLElement) {
137
+ // @ts-expect-error
138
+ container.current.style.transitionDuration = null;
139
+ }
140
+ const shouldClose = gestureState.dy > DELTA_Y_CLOSE_THRESHOLD || gestureState.vy > SWIPE_CLOSE_VELOCITY;
141
+ if (shouldClose) {
142
+ onPressClose();
143
+ }
144
+ else {
145
+ if (container.current instanceof HTMLElement) {
146
+ container.current.style.transform = `translateY(0px)`;
147
+ }
148
+ }
149
+ },
150
+ }), [onPressClose]);
89
151
  useBodyClassName("BottomPanelOpen", { enabled: visible });
90
152
  if (rootElement == null) {
91
153
  return null;
92
154
  }
93
- return (_jsxs(Portal, { container: rootElement, children: [_jsx(TransitionView, { style: styles.fill, enter: styles.overlayEnter, leave: styles.overlayLeave, children: visible ? _jsx(View, { style: styles.overlay }) : null }), _jsx(Suspense, { fallback: _jsx(LoadingView, { color: backgroundColor.accented, delay: 0 }), children: _jsx(TransitionView, { style: styles.fill, enter: styles.modalEnter, leave: styles.modalLeave, children: visible ? (_jsxs(ScrollView, { style: styles.modalContainer, contentContainerStyle: styles.modalContentContainer, children: [_jsx(FocusTrap, { autoFocus: true, focusLock: true, returnFocus: returnFocus, style: styles.trap, children: onPressClose != null ? (_jsx(Pressable, { onPress: onPressClose, style: styles.pressableOverlay })) : null }), _jsx(View, { style: styles.modal, children: children })] })) : null }) })] }));
155
+ return (_jsxs(Portal, { container: rootElement, children: [_jsx(TransitionView, { style: styles.fill, enter: styles.overlayEnter, leave: styles.overlayLeave, children: visible ? _jsx(View, { style: styles.overlay }) : null }), _jsx(Suspense, { fallback: _jsx(LoadingView, { color: backgroundColor.accented, delay: 0 }), children: _jsx(TransitionView, { style: styles.fill, enter: styles.modalEnter, leave: styles.modalLeave, children: visible ? (_jsxs(View, { ref: container, style: styles.container, children: [_jsx(ScrollView, { style: styles.modalContainer, contentContainerStyle: styles.modalContentContainer, children: _jsxs(FocusTrap, { autoFocus: true, focusLock: true, returnFocus: returnFocus, style: styles.trap, children: [onPressClose != null ? (_jsx(Pressable, { onPress: onPressClose, style: styles.pressableOverlay })) : null, _jsxs(View, { style: styles.modal, children: [_jsx(View, { style: styles.grabContainer, ...panResponder.panHandlers, children: _jsx(View, { style: styles.grabLine }) }), children] })] }) }), _jsx(View, { style: styles.bottomCache })] })) : null }) })] }));
94
156
  };
@@ -165,10 +165,10 @@ export const useCrumb = (crumb) => {
165
165
  }, [id, crumb, setValue, index]);
166
166
  };
167
167
  const CHEVRON = (_jsx(View, { style: styles.chevron, children: _jsx(Icon, { name: "chevron-right-filled", color: colors.gray[500], size: 16 }) }));
168
- const BreadcrumbsSiblingsDropdown = ({ siblings, onPress, }) => {
168
+ const BreadcrumbsSiblingsDropdown = ({ siblings, isLast, onPress, }) => {
169
169
  return (_jsx(View, { style: styles.siblingsDropdown, children: siblings.map(({ url, label, isMatching }) => {
170
170
  return (_jsx(Link, { to: url, ariaCurrentValue: "location", onPress: (event) => {
171
- if (isMatching) {
171
+ if (isMatching && isLast) {
172
172
  event.preventDefault();
173
173
  }
174
174
  onPress();
@@ -192,7 +192,7 @@ const BreadcrumbsItem = ({ crumb, isFirstItem = false, isLastItem = false, shoul
192
192
  return (_jsxs(View, { style: [styles.item, shouldAnimate ? animations.fadeAndSlideInFromRight.enter : null], children: [!isFirstItem ? CHEVRON : null, _jsxs(View, { children: [_jsx(Link, { to: crumb.link, ariaCurrentValue: "location", onPress: handlePress, children: _jsx(View, { ref: hoverRef, style: [
193
193
  styles.horizontalLink,
194
194
  shouldAnimate && animations.fadeAndSlideInFromRight.enter,
195
- ], children: _jsxs(LakeText, { color: colors.gray[800], style: [styles.horizontalLinkText, isLastItem && styles.activeHorizontalLinkText], children: [_jsx(Text, { style: isHovered && !isLastItem ? styles.horizontalLinkTextHovered : undefined, children: crumb.label }), crumb.siblings != null ? (_jsxs(_Fragment, { children: [_jsx(Space, { width: 4 }), _jsx(Icon, { name: "chevron-down-filled", color: colors.gray[500], size: 16 })] })) : null] }) }) }), _jsx(View, { style: styles.dropdownContainer, children: _jsx(TransitionView, { ...animations.fadeAndSlideInFromBottom, children: siblings ? (_jsx(FocusTrap, { autoFocus: true, focusLock: true, returnFocus: true, onClickOutside: () => setSiblings(null), onEscapeKey: () => setSiblings(null), children: _jsx(BreadcrumbsSiblingsDropdown, { siblings: siblings, onPress: () => setSiblings(null) }) })) : null }) })] })] }));
195
+ ], children: _jsxs(LakeText, { color: colors.gray[800], style: [styles.horizontalLinkText, isLastItem && styles.activeHorizontalLinkText], children: [_jsx(Text, { style: isHovered && !isLastItem ? styles.horizontalLinkTextHovered : undefined, children: crumb.label }), crumb.siblings != null ? (_jsxs(_Fragment, { children: [_jsx(Space, { width: 4 }), _jsx(Icon, { name: "chevron-down-filled", color: colors.gray[500], size: 16 })] })) : null] }) }) }), _jsx(View, { style: styles.dropdownContainer, children: _jsx(TransitionView, { ...animations.fadeAndSlideInFromBottom, children: siblings ? (_jsx(FocusTrap, { autoFocus: true, focusLock: true, returnFocus: true, onClickOutside: () => setSiblings(null), onEscapeKey: () => setSiblings(null), children: _jsx(BreadcrumbsSiblingsDropdown, { siblings: siblings, isLast: isLastItem, onPress: () => setSiblings(null) }) })) : null }) })] })] }));
196
196
  };
197
197
  const BreadcrumbsDropdown = ({ crumbs, onHoverStart, onHoverEnd, onLinkFocus, onLinkBlur, onLinkPress, }) => {
198
198
  const containerRef = useRef(null);
@@ -1,8 +1,11 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from "react";
3
- import { StyleSheet, View } from "react-native";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { ScrollView, StyleSheet, View } from "react-native";
4
+ import { match } from "ts-pattern";
4
5
  import { breakpoints, negativeSpacings, spacings } from "../constants/design";
5
6
  import { useResponsive } from "../hooks/useResponsive";
7
+ import { clampValue } from "../utils/math";
8
+ import { detectScrollAnimationEnd } from "../utils/viewport";
6
9
  import { LakeButton } from "./LakeButton";
7
10
  import { LakeRadio } from "./LakeRadio";
8
11
  import { Pressable } from "./Pressable";
@@ -16,6 +19,9 @@ const styles = StyleSheet.create({
16
19
  overflow: "hidden",
17
20
  marginHorizontal: negativeSpacings[12],
18
21
  },
22
+ scrollSnap: {
23
+ scrollSnapType: "x mandatory",
24
+ },
19
25
  container: {
20
26
  alignSelf: "stretch",
21
27
  flexDirection: "row",
@@ -35,6 +41,8 @@ const styles = StyleSheet.create({
35
41
  flexBasis: "33.333%",
36
42
  maxWidth: 300,
37
43
  padding: spacings[12],
44
+ },
45
+ itemAnimation: {
38
46
  transform: "translateZ(0px)",
39
47
  animationKeyframes: {
40
48
  from: {
@@ -58,6 +66,7 @@ const styles = StyleSheet.create({
58
66
  width: "100%",
59
67
  flexBasis: "auto",
60
68
  maxWidth: "none",
69
+ scrollSnapAlign: "center",
61
70
  },
62
71
  tileContents: {
63
72
  alignItems: "center",
@@ -74,26 +83,108 @@ const styles = StyleSheet.create({
74
83
  top: "50%",
75
84
  left: negativeSpacings[24],
76
85
  transform: "translateY(-50%)",
86
+ borderTopLeftRadius: 0,
87
+ borderBottomLeftRadius: 0,
88
+ borderWidth: 1,
89
+ borderLeftWidth: 0,
77
90
  },
78
91
  rightButton: {
79
92
  position: "absolute",
80
93
  top: "50%",
81
94
  right: negativeSpacings[24],
82
95
  transform: "translateY(-50%)",
96
+ borderTopRightRadius: 0,
97
+ borderBottomRightRadius: 0,
98
+ borderWidth: 1,
99
+ borderRightWidth: 0,
83
100
  },
84
101
  });
85
102
  const identity = (x) => x;
86
103
  export const ChoicePicker = ({ items, getId = identity, large = false, renderItem, value, onChange, }) => {
104
+ const containerRef = useRef(null);
87
105
  const { desktop } = useResponsive(breakpoints.medium);
88
- const [index, setIndex] = useState(0);
89
- return (_jsxs(View, { children: [_jsx(View, { style: styles.root, children: _jsx(View, { style: [
106
+ const [mobilePosition, setMobilePosition] = useState("start");
107
+ useEffect(() => {
108
+ if (desktop) {
109
+ return;
110
+ }
111
+ // auto scroll to selected value on mobile
112
+ const scrollContainer = containerRef.current;
113
+ const index = items.findIndex(item => value === item);
114
+ if (index !== -1 && scrollContainer instanceof HTMLDivElement) {
115
+ const width = scrollContainer.offsetWidth;
116
+ scrollContainer.scrollTo({ x: index * width, animated: false });
117
+ }
118
+ // if no value is selected, select first item
119
+ if (value == null && items[0] != null) {
120
+ onChange(items[0]);
121
+ }
122
+ // disable exhaustive-deps because we only want to run this effect only when screen size go from desktop to mobile
123
+ }, [desktop]); // eslint-disable-line react-hooks/exhaustive-deps
124
+ const onScroll = () => {
125
+ // prevent scroll event when we change screen size from mobile to desktop
126
+ if (desktop) {
127
+ return;
128
+ }
129
+ const scrollContainer = containerRef.current;
130
+ if (scrollContainer instanceof HTMLDivElement) {
131
+ const scrollLeft = scrollContainer.scrollLeft;
132
+ const width = scrollContainer.offsetWidth;
133
+ const index = clampValue(0, items.length - 1)(Math.round(scrollLeft / width));
134
+ const item = items[index];
135
+ if (item != null) {
136
+ onChange(item);
137
+ }
138
+ match(index)
139
+ .with(0, () => setMobilePosition("start"))
140
+ .with(items.length - 1, () => setMobilePosition("end"))
141
+ .otherwise(() => setMobilePosition("middle"));
142
+ }
143
+ };
144
+ const onPressPrevious = () => {
145
+ const scrollContainer = containerRef.current;
146
+ if (scrollContainer instanceof HTMLDivElement) {
147
+ const scrollLeft = scrollContainer.scrollLeft;
148
+ const width = scrollContainer.offsetWidth;
149
+ const index = Math.round(scrollLeft / width);
150
+ const previousIndex = Math.max(0, index - 1);
151
+ // remove scroll snap during scroll animation to avoid weird behavior on older browsers
152
+ scrollContainer.style.scrollSnapType = "none";
153
+ containerRef.current?.scrollTo({ x: previousIndex * width, animated: true });
154
+ detectScrollAnimationEnd(scrollContainer).onResolve(() => {
155
+ // set back scroll snap
156
+ // @ts-expect-error
157
+ scrollContainer.style.scrollSnapType = null;
158
+ });
159
+ }
160
+ };
161
+ const onPressNext = () => {
162
+ const scrollContainer = containerRef.current;
163
+ if (scrollContainer instanceof HTMLDivElement) {
164
+ const scrollLeft = scrollContainer.scrollLeft;
165
+ const width = scrollContainer.offsetWidth;
166
+ const index = Math.round(scrollLeft / width);
167
+ const nextIndex = Math.min(items.length - 1, index + 1);
168
+ // remove scroll snap during scroll animation to avoid weird behavior on older browsers
169
+ scrollContainer.style.scrollSnapType = "none";
170
+ containerRef.current?.scrollTo({ x: nextIndex * width, animated: true });
171
+ detectScrollAnimationEnd(scrollContainer).onResolve(() => {
172
+ // set back scroll snap
173
+ // @ts-expect-error
174
+ scrollContainer.style.scrollSnapType = null;
175
+ });
176
+ }
177
+ };
178
+ return (_jsxs(View, { children: [_jsx(View, { style: styles.root, children: _jsx(ScrollView, { ref: containerRef, horizontal: !desktop, onScroll: onScroll, scrollEventThrottle: 200, style: styles.scrollSnap, contentContainerStyle: [
90
179
  styles.container,
91
180
  !desktop && styles.mobileContainer,
92
- !desktop && { transform: `translateX(-${100 * index}%)` },
181
+ !desktop && { width: `${items.length * 100}%` },
93
182
  ], children: items.map((item, index) => (_jsx(Pressable, { style: [
94
183
  styles.item,
184
+ desktop && styles.itemAnimation,
185
+ desktop && { animationDelay: `${200 + 100 * index}ms` },
95
186
  large && styles.itemLarge,
96
187
  !desktop && styles.itemSmallViewport,
97
- { animationDelay: `${200 + 100 * index}ms` },
98
- ], onPress: () => onChange(item), children: ({ hovered }) => (_jsx(Tile, { hovered: hovered, selected: value != null && getId(item) === getId(value), flexGrow: 1, children: _jsxs(View, { style: styles.tileContents, children: [_jsx(View, { style: styles.tileRenderedContents, children: renderItem(item) }), _jsx(Space, { height: 24 }), _jsx(LakeRadio, { value: value != null && getId(item) === getId(value) })] }) })) }, String(index)))) }) }), !desktop && (_jsx(View, { style: styles.leftButton, children: _jsx(LakeButton, { icon: "chevron-left-filled", mode: "secondary", forceBackground: true, onPress: () => setIndex(Math.max(0, index - 1)), disabled: index === 0 }) })), !desktop && (_jsx(View, { style: styles.rightButton, children: _jsx(LakeButton, { icon: "chevron-right-filled", mode: "secondary", forceBackground: true, onPress: () => setIndex(Math.min(items.length - 1, index + 1)), disabled: index === items.length - 1 }) }))] }));
188
+ !desktop && { width: `${100 / items.length}%` },
189
+ ], onPress: () => onChange(item), children: ({ hovered }) => (_jsx(Tile, { hovered: hovered, selected: value != null && getId(item) === getId(value), flexGrow: 1, children: _jsxs(View, { style: styles.tileContents, children: [_jsx(View, { style: styles.tileRenderedContents, children: renderItem(item) }), desktop && (_jsxs(_Fragment, { children: [_jsx(Space, { height: 24 }), _jsx(LakeRadio, { value: value != null && getId(item) === getId(value) })] }))] }) })) }, String(index)))) }) }), !desktop && (_jsx(LakeButton, { icon: "chevron-left-filled", mode: "secondary", forceBackground: true, onPress: onPressPrevious, disabled: mobilePosition === "start", style: styles.leftButton })), !desktop && (_jsx(LakeButton, { icon: "chevron-right-filled", mode: "secondary", forceBackground: true, onPress: onPressNext, disabled: mobilePosition === "end", style: styles.rightButton }))] }));
99
190
  };
@@ -193,6 +193,10 @@ function FilterInput({ label, initialValue = "", noValueText, submitText, autoOp
193
193
  };
194
194
  return (_jsxs(View, { style: styles.container, children: [_jsx(FilterTag, { label: label, onPress: toggle, ref: inputRef, onPressRemove: onPressRemove, isActive: visible, value: value === "" ? noValueText : value }), _jsx(Popover, { role: "listbox", matchReferenceWidth: false, onDismiss: close, referenceRef: inputRef, returnFocus: false, visible: visible, children: _jsxs(View, { style: [styles.dropdown, styles.inputContent], children: [_jsx(Field, { name: "input", children: ({ error, value, onChange }) => (_jsx(LakeLabel, { label: label, render: id => (_jsx(LakeTextInput, { nativeID: id, error: error, style: styles.input, placeholder: placeholder, value: value, onChangeText: onChange })) })) }), _jsx(LakeButton, { size: "small", color: "current", onPress: onSubmit, children: submitText })] }) })] }));
195
195
  }
196
+ function FilterBooleanTag({ children, onAdd, onPressRemove }) {
197
+ useEffect(onAdd, []); // eslint-disable-line react-hooks/exhaustive-deps
198
+ return (_jsx(Tag, { color: "current", onPressRemove: onPressRemove, children: children }));
199
+ }
196
200
  const getFilterValue = (_type, filters, name) => filters[name];
197
201
  export const FiltersStack = ({ filters, openedFilters, definition, onChangeOpened, onChangeFilters, }) => {
198
202
  const previousOpened = usePreviousValue(openedFilters);
@@ -224,7 +228,9 @@ export const FiltersStack = ({ filters, openedFilters, definition, onChangeOpene
224
228
  onChangeFilters({ ...filters, [filterName]: undefined });
225
229
  onChangeOpened(openedFilters.filter(f => f !== filterName));
226
230
  } })))
227
- .with({ type: "boolean" }, ({ label }) => (_jsx(Tag, { color: "current", onPressRemove: () => {
231
+ .with({ type: "boolean" }, ({ label }) => (_jsx(FilterBooleanTag, { onAdd: () => {
232
+ onChangeFilters({ ...filters, [filterName]: true });
233
+ }, onPressRemove: () => {
228
234
  onChangeFilters({ ...filters, [filterName]: undefined });
229
235
  onChangeOpened(openedFilters.filter(f => f !== filterName));
230
236
  }, children: label })))
@@ -1,7 +1,126 @@
1
1
  import { ReactNode } from "react";
2
- import { View, ViewProps } from "react-native";
3
- import { Except } from "type-fest";
4
- export declare const Form: import("react").MemoExoticComponent<import("react").ForwardRefExoticComponent<Except<ViewProps, "role"> & {
2
+ import { View } from "react-native";
3
+ export declare const Form: import("react").MemoExoticComponent<import("react").ForwardRefExoticComponent<{
4
+ children?: ReactNode;
5
+ hitSlop?: import("react-native").Insets | undefined;
6
+ id?: string | undefined;
7
+ onLayout?: ((event: import("react-native").LayoutChangeEvent) => void) | undefined;
8
+ pointerEvents?: "auto" | "none" | "box-none" | "box-only" | undefined;
9
+ removeClippedSubviews?: boolean | undefined;
10
+ style?: import("react-native").StyleProp<import("react-native").ViewStyle>;
11
+ testID?: string | undefined;
12
+ nativeID?: string | undefined;
13
+ onKeyDown?: ((event: NativeSyntheticEvent<import("react").KeyboardEvent<Element>>) => void) | undefined;
14
+ onKeyDownCapture?: ((event: NativeSyntheticEvent<import("react").KeyboardEvent<Element>>) => void) | undefined;
15
+ onKeyUp?: ((event: NativeSyntheticEvent<import("react").KeyboardEvent<Element>>) => void) | undefined;
16
+ onKeyUpCapture?: ((event: NativeSyntheticEvent<import("react").KeyboardEvent<Element>>) => void) | undefined;
17
+ collapsable?: boolean | undefined;
18
+ needsOffscreenAlphaCompositing?: boolean | undefined;
19
+ renderToHardwareTextureAndroid?: boolean | undefined;
20
+ focusable?: boolean | undefined;
21
+ shouldRasterizeIOS?: boolean | undefined;
22
+ isTVSelectable?: boolean | undefined;
23
+ hasTVPreferredFocus?: boolean | undefined;
24
+ tvParallaxProperties?: import("react-native").TVParallaxProperties | undefined;
25
+ tvParallaxShiftDistanceX?: number | undefined;
26
+ tvParallaxShiftDistanceY?: number | undefined;
27
+ tvParallaxTiltAngle?: number | undefined;
28
+ tvParallaxMagnification?: number | undefined;
29
+ onStartShouldSetResponder?: ((event: import("react-native").GestureResponderEvent) => boolean) | undefined;
30
+ onMoveShouldSetResponder?: ((event: import("react-native").GestureResponderEvent) => boolean) | undefined;
31
+ onResponderEnd?: ((event: import("react-native").GestureResponderEvent) => void) | undefined;
32
+ onResponderGrant?: ((event: import("react-native").GestureResponderEvent) => void) | undefined;
33
+ onResponderReject?: ((event: import("react-native").GestureResponderEvent) => void) | undefined;
34
+ onResponderMove?: ((event: import("react-native").GestureResponderEvent) => void) | undefined;
35
+ onResponderRelease?: ((event: import("react-native").GestureResponderEvent) => void) | undefined;
36
+ onResponderStart?: ((event: import("react-native").GestureResponderEvent) => void) | undefined;
37
+ onResponderTerminationRequest?: ((event: import("react-native").GestureResponderEvent) => boolean) | undefined;
38
+ onResponderTerminate?: ((event: import("react-native").GestureResponderEvent) => void) | undefined;
39
+ onStartShouldSetResponderCapture?: ((event: import("react-native").GestureResponderEvent) => boolean) | undefined;
40
+ onMoveShouldSetResponderCapture?: ((event: import("react-native").GestureResponderEvent) => boolean) | undefined;
41
+ onTouchStart?: ((event: import("react-native").GestureResponderEvent) => void) | undefined;
42
+ onTouchMove?: ((event: import("react-native").GestureResponderEvent) => void) | undefined;
43
+ onTouchEnd?: ((event: import("react-native").GestureResponderEvent) => void) | undefined;
44
+ onTouchCancel?: ((event: import("react-native").GestureResponderEvent) => void) | undefined;
45
+ onTouchEndCapture?: ((event: import("react-native").GestureResponderEvent) => void) | undefined;
46
+ onPointerEnter?: ((event: import("react-native").PointerEvent) => void) | undefined;
47
+ onPointerEnterCapture?: ((event: import("react-native").PointerEvent) => void) | undefined;
48
+ onPointerLeave?: ((event: import("react-native").PointerEvent) => void) | undefined;
49
+ onPointerLeaveCapture?: ((event: import("react-native").PointerEvent) => void) | undefined;
50
+ onPointerMove?: ((event: import("react-native").PointerEvent) => void) | undefined;
51
+ onPointerMoveCapture?: ((event: import("react-native").PointerEvent) => void) | undefined;
52
+ onPointerCancel?: ((event: import("react-native").PointerEvent) => void) | undefined;
53
+ onPointerCancelCapture?: ((event: import("react-native").PointerEvent) => void) | undefined;
54
+ onPointerDown?: ((event: import("react-native").PointerEvent) => void) | undefined;
55
+ onPointerDownCapture?: ((event: import("react-native").PointerEvent) => void) | undefined;
56
+ onPointerUp?: ((event: import("react-native").PointerEvent) => void) | undefined;
57
+ onPointerUpCapture?: ((event: import("react-native").PointerEvent) => void) | undefined;
58
+ accessible?: boolean | undefined;
59
+ accessibilityActions?: readonly Readonly<{
60
+ name: string;
61
+ label?: string | undefined;
62
+ }>[] | undefined;
63
+ accessibilityLabel?: string | undefined;
64
+ 'aria-label'?: string | undefined;
65
+ accessibilityRole?: import("react-native").AccessibilityRole | undefined;
66
+ accessibilityState?: import("react-native").AccessibilityState | undefined;
67
+ 'aria-busy'?: boolean | undefined;
68
+ 'aria-checked'?: boolean | "mixed" | undefined;
69
+ 'aria-disabled'?: boolean | undefined;
70
+ 'aria-expanded'?: boolean | undefined;
71
+ 'aria-selected'?: boolean | undefined;
72
+ 'aria-labelledby'?: string | undefined;
73
+ accessibilityHint?: string | undefined;
74
+ accessibilityValue?: import("react-native").AccessibilityValue | undefined;
75
+ 'aria-valuemax'?: number | undefined;
76
+ 'aria-valuemin'?: number | undefined;
77
+ 'aria-valuenow'?: number | undefined;
78
+ 'aria-valuetext'?: string | undefined;
79
+ onAccessibilityAction?: ((event: import("react-native").AccessibilityActionEvent) => void) | undefined;
80
+ importantForAccessibility?: "auto" | "yes" | "no" | "no-hide-descendants" | undefined;
81
+ 'aria-hidden'?: boolean | undefined;
82
+ 'aria-live'?: "polite" | "assertive" | "off" | undefined;
83
+ 'aria-modal'?: boolean | undefined;
84
+ accessibilityLiveRegion?: "none" | "polite" | "assertive" | undefined;
85
+ accessibilityElementsHidden?: boolean | undefined;
86
+ accessibilityViewIsModal?: boolean | undefined;
87
+ onAccessibilityEscape?: (() => void) | undefined;
88
+ onAccessibilityTap?: (() => void) | undefined;
89
+ onMagicTap?: (() => void) | undefined;
90
+ accessibilityIgnoresInvertColors?: boolean | undefined;
91
+ tabIndex?: 0 | -1 | undefined;
92
+ "aria-activedescendant"?: string | undefined;
93
+ "aria-atomic"?: boolean | undefined;
94
+ "aria-autocomplete"?: string | undefined;
95
+ "aria-colcount"?: number | undefined;
96
+ "aria-colindex"?: number | undefined;
97
+ "aria-colspan"?: number | undefined;
98
+ "aria-controls"?: string | undefined;
99
+ "aria-current"?: boolean | "time" | "page" | "step" | "location" | "date" | undefined;
100
+ "aria-describedby"?: string | undefined;
101
+ "aria-details"?: string | undefined;
102
+ "aria-errormessage"?: string | undefined;
103
+ "aria-flowto"?: string | undefined;
104
+ "aria-haspopup"?: string | undefined;
105
+ "aria-invalid"?: boolean | undefined;
106
+ "aria-keyshortcuts"?: string | undefined;
107
+ "aria-level"?: number | undefined;
108
+ "aria-multiline"?: boolean | undefined;
109
+ "aria-multiselectable"?: boolean | undefined;
110
+ "aria-orientation"?: "horizontal" | "vertical" | undefined;
111
+ "aria-owns"?: string | undefined;
112
+ "aria-placeholder"?: string | undefined;
113
+ "aria-posinset"?: number | undefined;
114
+ "aria-pressed"?: boolean | undefined;
115
+ "aria-readonly"?: boolean | undefined;
116
+ "aria-required"?: boolean | undefined;
117
+ "aria-roledescription"?: string | undefined;
118
+ "aria-rowcount"?: number | undefined;
119
+ "aria-rowindex"?: number | undefined;
120
+ "aria-rowspan"?: number | undefined;
121
+ "aria-setsize"?: number | undefined;
122
+ "aria-sort"?: "none" | "ascending" | "descending" | "other" | undefined;
123
+ } & {
5
124
  children?: ReactNode;
6
125
  onReset?: ((event: React.FormEvent<HTMLElement>) => void) | undefined;
7
126
  onSubmit?: ((event: React.FormEvent<HTMLElement>) => void) | undefined;