@swan-io/lake 13.10.2 → 13.11.1

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": "13.10.2",
3
+ "version": "13.11.1",
4
4
  "engines": {
5
5
  "node": ">22.12.0"
6
6
  },
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
3
- import { StyleSheet, TextInput, View, } from "react-native";
3
+ import { StyleSheet, Text, TextInput, View, } from "react-native";
4
+ import { t } from "../../../shared-business/src/utils/i18n";
4
5
  import { commonStyles } from "../constants/commonStyles";
5
6
  import { backgroundColor, colors, invariantColors, radii, shadows, spacings, texts, } from "../constants/design";
6
7
  import { useBoolean } from "../hooks/useBoolean";
@@ -145,6 +146,12 @@ const styles = StyleSheet.create({
145
146
  list: {
146
147
  maxHeight: ITEM_ELEMENT_HEIGHT * NB_SUGGESTION_DISPLAYED,
147
148
  },
149
+ emptyList: {
150
+ height: 136,
151
+ },
152
+ emptyListText: {
153
+ color: colors.gray.primary,
154
+ },
148
155
  });
149
156
  export const LakeSelect = ({ ref, title, items, valueStyle, size, color = "current", disabled = false, mode = "normal", placeholder, readOnly = false, id, matchReferenceMinWidth = true, matchReferenceWidth = false, value, error, hideErrors = false, icon, onValueChange, disabledItems = [], renderItem, PopoverFooter, style, hasSearch = false, searchPlaceholder, }) => {
150
157
  var _a;
@@ -212,6 +219,7 @@ export const LakeSelect = ({ ref, title, items, valueStyle, size, color = "curre
212
219
  setHighlightApi(filter, (_a = listRef.current) === null || _a === void 0 ? void 0 : _a.element);
213
220
  }, [filter, hasSearch]);
214
221
  const ListHeaderComponent = useMemo(() => (_jsxs(Box, { direction: "row", alignItems: "center", style: styles.filterContainer, children: [_jsx(TextInput, { autoComplete: "off", inputMode: "search", multiline: false, rows: 1, onChangeText: filterValue => setFilter(filterValue), placeholder: searchPlaceholder, value: filter, onFocus: setFilterFocused.on, onBlur: setFilterFocused.off, style: [styles.filterInput, filterFocused && styles.filterFocused] }), _jsx(Icon, { name: "search-filled", color: colors[color].primary, size: 20, style: styles.searchIcon })] })), [filter, filterFocused, setFilterFocused, searchPlaceholder, color]);
222
+ const ListEmptyComponent = (_jsxs(Box, { justifyContent: "center", alignItems: "center", style: styles.emptyList, children: [_jsx(Icon, { name: "clipboard-search-regular", size: 24, color: colors.gray.primary }), _jsx(Space, { height: 8 }), _jsx(Text, { style: styles.emptyListText, children: t("common.noResult") })] }));
215
223
  return (_jsxs(View, { style: commonStyles.fill, children: [_jsx(Pressable, { id: id, ref: mergedRef, "aria-haspopup": "listbox", role: "button", "aria-expanded": visible, disabled: readOnly || disabled, style: ({ focused, hovered, pressed }) => [
216
224
  mode === "normal" ? styles.normal : styles.borderless,
217
225
  size === "small" && styles.small,
@@ -234,7 +242,7 @@ export const LakeSelect = ({ ref, title, items, valueStyle, size, color = "curre
234
242
  styles.itemText,
235
243
  styles.selectPlaceholder,
236
244
  isSmall && styles.selectSmallPlaceholder,
237
- ], children: placeholder !== null && placeholder !== void 0 ? placeholder : " " }))] }), _jsx(Fill, { minWidth: 8 }), !disabled && (_jsx(Icon, { color: colors.gray[900], name: visible ? "chevron-up-filled" : "chevron-down-filled", size: 16 }))] })] })) }), !hideErrors && (_jsx(LakeText, { variant: "smallRegular", color: colors.negative[500], style: styles.errorText, children: error !== null && error !== void 0 ? error : " " })), _jsxs(Popover, { role: "listbox", matchReferenceWidth: matchReferenceWidth, matchReferenceMinWidth: matchReferenceMinWidth, onDismiss: close, referenceRef: inputRef, returnFocus: true, visible: visible, children: [hasSearch ? ListHeaderComponent : undefined, _jsxs(View, { style: styles.list, children: [isNotNullish(title) && (_jsxs(_Fragment, { children: [_jsx(LakeText, { variant: "semibold", color: colors.gray[900], style: styles.selectListTitle, children: title }), _jsx(Separator, {})] })), _jsx(FlatList, { role: "list", data: filteredItems, ref: listRef, contentContainerStyle: styles.listContent, onKeyDown: (event) => {
245
+ ], children: placeholder !== null && placeholder !== void 0 ? placeholder : " " }))] }), _jsx(Fill, { minWidth: 8 }), !disabled && (_jsx(Icon, { color: colors.gray[900], name: visible ? "chevron-up-filled" : "chevron-down-filled", size: 16 }))] })] })) }), !hideErrors && (_jsx(LakeText, { variant: "smallRegular", color: colors.negative[500], style: styles.errorText, children: error !== null && error !== void 0 ? error : " " })), _jsxs(Popover, { role: "listbox", matchReferenceWidth: matchReferenceWidth, matchReferenceMinWidth: matchReferenceMinWidth, onDismiss: close, referenceRef: inputRef, returnFocus: true, visible: visible, children: [hasSearch ? ListHeaderComponent : undefined, _jsxs(View, { style: styles.list, children: [isNotNullish(title) && (_jsxs(_Fragment, { children: [_jsx(LakeText, { variant: "semibold", color: colors.gray[900], style: styles.selectListTitle, children: title }), _jsx(Separator, {})] })), _jsx(FlatList, { role: "list", data: filteredItems, ref: listRef, contentContainerStyle: styles.listContent, ListEmptyComponent: ListEmptyComponent, onKeyDown: (event) => {
238
246
  var _a;
239
247
  const { key } = event.nativeEvent;
240
248
  if (key === "ArrowDown" || key === "ArrowUp") {
@@ -8,8 +8,9 @@ export type Item<T extends string> = {
8
8
  type Props<T extends string> = {
9
9
  selected: T;
10
10
  items: ReadonlyArray<Item<T>>;
11
+ fullWidth?: boolean;
12
+ mobileBreakpoint?: number;
11
13
  onValueChange: (value: T) => void;
12
- minItemWidth?: number;
13
14
  };
14
- export declare const SegmentedControl: <T extends string>({ selected, items, onValueChange, minItemWidth, }: Props<T>) => import("react/jsx-runtime").JSX.Element;
15
+ export declare const SegmentedControl: <T extends string>(props: Props<T>) => import("react/jsx-runtime").JSX.Element;
15
16
  export {};
@@ -1,45 +1,56 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useState } from "react";
3
- import { StyleSheet, View } from "react-native";
4
- import { P, match } from "ts-pattern";
5
- import { backgroundColor, colors, radii, spacings, texts } from "../constants/design";
6
- import { isNotNullish } from "../utils/nullish";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Pressable, StyleSheet, View } from "react-native";
4
+ import { backgroundColor, breakpoints, colors, invariantColors, radii, spacings, } from "../constants/design";
7
5
  import { BottomPanel } from "./BottomPanel";
8
6
  import { Box } from "./Box";
9
7
  import { Icon } from "./Icon";
10
8
  import { LakeButton } from "./LakeButton";
11
9
  import { LakeText } from "./LakeText";
12
- import { Pressable } from "./Pressable";
13
10
  import { ResponsiveContainer } from "./ResponsiveContainer";
14
11
  import { Space } from "./Space";
12
+ const HORIZONTAL_PADDING = 4;
15
13
  const styles = StyleSheet.create({
14
+ responsiveContainer: {
15
+ flexDirection: "row",
16
+ justifyContent: "center",
17
+ },
16
18
  container: {
17
- padding: spacings[4],
19
+ flexDirection: "row",
20
+ alignItems: "center",
18
21
  backgroundColor: colors.gray[50],
19
- borderRadius: radii[8],
22
+ borderRadius: 30,
23
+ paddingVertical: 4,
24
+ paddingHorizontal: HORIZONTAL_PADDING,
20
25
  },
21
- selectedItemIndicator: {
26
+ containerFullWidth: {
27
+ width: "100%",
28
+ },
29
+ selectedIndicator: {
22
30
  position: "absolute",
23
- left: 0,
24
- top: 0,
25
- flexGrow: 1,
26
- flexShrink: 1,
27
- transitionProperty: "transform",
28
- transitionDuration: "250ms",
31
+ top: 4,
32
+ bottom: 4,
33
+ backgroundColor: invariantColors.white,
34
+ borderRadius: 30,
35
+ transitionProperty: "transform, width",
29
36
  transitionTimingFunction: "ease",
30
- padding: spacings[4],
31
- borderRadius: radii[4],
32
- backgroundColor: backgroundColor.accented,
33
37
  },
34
- itemMobile: {
35
- backgroundColor: backgroundColor.accented,
36
- borderRadius: radii[4],
37
- padding: spacings[16],
38
+ fill: {
39
+ flex: 1,
40
+ },
41
+ item: {
38
42
  flexDirection: "row",
39
- height: 60,
40
- alignItems: "center",
41
43
  justifyContent: "center",
42
- flexGrow: 1,
44
+ alignItems: "center",
45
+ paddingVertical: 12,
46
+ paddingHorizontal: 12,
47
+ borderRadius: 30,
48
+ },
49
+ itemSelected: {
50
+ backgroundColor: invariantColors.white,
51
+ },
52
+ button: {
53
+ borderRadius: 30,
43
54
  },
44
55
  dropdownItem: {
45
56
  backgroundColor: backgroundColor.accented,
@@ -48,71 +59,70 @@ const styles = StyleSheet.create({
48
59
  flexDirection: "row",
49
60
  height: 60,
50
61
  alignItems: "center",
51
- flexGrow: 1,
62
+ flex: 1,
52
63
  },
53
64
  dropdownItemSelected: {
54
65
  backgroundColor: colors.gray[50],
55
- borderRadius: radii[4],
56
- padding: spacings[16],
57
- flexDirection: "row",
58
- height: 60,
59
- alignItems: "center",
60
- flexGrow: 1,
61
- },
62
- button: {
63
- width: 60,
64
- height: 60,
65
66
  },
66
- itemDesktop: {
67
- flexBasis: "0%",
68
- flexGrow: 1,
69
- flexShrink: 1,
67
+ dropdownCheckIconContainer: {
70
68
  alignItems: "center",
71
- padding: spacings[16],
72
- flexDirection: "row",
73
69
  justifyContent: "center",
74
- },
75
- itemText: {
76
- userSelect: "none",
77
- lineHeight: texts.regular.fontSize,
78
- },
79
- selectedItemDesktop: {
80
- bottom: 0,
70
+ paddingHorizontal: spacings[24],
71
+ backgroundColor: colors.gray[50],
81
72
  },
82
73
  });
83
- export const SegmentedControl = ({ selected, items, onValueChange, minItemWidth = 250, }) => {
74
+ export const SegmentedControl = (props) => {
75
+ const { mobileBreakpoint = breakpoints.small } = props;
76
+ return (_jsx(ResponsiveContainer, { breakpoint: mobileBreakpoint, style: styles.responsiveContainer, children: ({ small }) => small ? _jsx(SegmentedControlMobile, { ...props }) : _jsx(SegmentedControlDesktop, { ...props }) }));
77
+ };
78
+ const SegmentedControlDesktop = ({ selected, items, fullWidth = false, onValueChange, }) => {
79
+ var _a;
84
80
  const selectedItemIndex = items.findIndex(item => item.id === selected);
85
- const selectedItem = items[selectedItemIndex];
86
- const [pressed, setPressed] = useState(false);
87
- return (_jsx(ResponsiveContainer, { breakpoint: items.length * minItemWidth, style: styles.container, children: ({ small }) => small ? (_jsxs(Box, { direction: "row", alignItems: "center", justifyContent: "spaceBetween", children: [_jsxs(Pressable, { style: styles.itemMobile, onPress: () => {
88
- setPressed(true);
89
- }, children: [isNotNullish(selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.icon) &&
90
- match(selectedItem)
91
- .with({ icon: P.nonNullable, activeIcon: P.nonNullable }, () => selectedItem.activeIcon)
92
- .with({ icon: P.nonNullable }, () => selectedItem.icon)
93
- .otherwise(() => null), _jsx(Space, { height: 8, width: 12 }), _jsx(LakeText, { color: colors.gray[900], numberOfLines: 1, variant: "regular", style: styles.itemText, children: selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.name })] }), _jsx(BottomPanel, { visible: pressed === true, onPressClose: () => {
94
- setPressed(false);
95
- }, children: items.map(item => (_jsxs(Box, { direction: "row", children: [_jsxs(Pressable, { style: (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id) === item.id
96
- ? styles.dropdownItemSelected
97
- : styles.dropdownItem, onPress: () => {
81
+ const [itemSizes, setItemSizes] = useState([]);
82
+ const indicatorPosition = (_a = itemSizes[selectedItemIndex]) !== null && _a !== void 0 ? _a : { left: 0, width: 0 };
83
+ const [indicatorRendered, setIndicatorRendered] = useState(false); // use to prevent animation on first render
84
+ const updateItemSize = (event) => {
85
+ // @ts-expect-error target exists in react-native-web
86
+ const target = event.nativeEvent.target;
87
+ const sizes = Array.from(target.children)
88
+ .slice(1) // first child is the selected indicator
89
+ .map(child => ({
90
+ left: child.offsetLeft - HORIZONTAL_PADDING,
91
+ width: child.offsetWidth,
92
+ }));
93
+ setItemSizes(sizes);
94
+ };
95
+ useEffect(() => {
96
+ if (indicatorPosition.width > 0) {
97
+ setIndicatorRendered(true);
98
+ }
99
+ }, [indicatorPosition]);
100
+ return (_jsxs(View, { style: [styles.container, fullWidth && styles.containerFullWidth], onLayout: updateItemSize, children: [_jsx(View, { role: "none", style: [
101
+ styles.selectedIndicator,
102
+ indicatorPosition
103
+ ? {
104
+ transitionDuration: indicatorRendered ? "250ms" : "0ms",
105
+ transform: `translateX(${indicatorPosition.left}px)`,
106
+ width: indicatorPosition.width,
107
+ }
108
+ : null,
109
+ ] }), items.map(item => {
110
+ const isSelected = item.id === selected;
111
+ return (_jsxs(Pressable, { style: [styles.item, fullWidth && styles.fill], onPress: () => onValueChange(item.id), children: [item.icon != null && (_jsxs(_Fragment, { children: [isSelected && item.activeIcon != null ? item.activeIcon : item.icon, _jsx(Space, { width: 12 })] })), _jsx(LakeText, { color: isSelected ? colors.current[500] : colors.gray[500], children: item.name })] }, item.id));
112
+ })] }));
113
+ };
114
+ const SegmentedControlMobile = ({ selected, items, onValueChange }) => {
115
+ var _a;
116
+ // biome-ignore lint/style/noNonNullAssertion: we're sure to have at least 2 items
117
+ const selectedItem = (_a = items.find(item => item.id === selected)) !== null && _a !== void 0 ? _a : items[0];
118
+ const [menuOpen, setMenuOpen] = useState(false);
119
+ return (_jsxs(View, { style: [styles.container, styles.containerFullWidth], children: [_jsxs(Pressable, { style: [styles.item, styles.itemSelected, styles.fill], onPress: () => setMenuOpen(true), children: [selectedItem.icon != null && (_jsxs(_Fragment, { children: [selectedItem.activeIcon != null ? selectedItem.activeIcon : selectedItem.icon, _jsx(Space, { width: 12 })] })), _jsx(LakeText, { color: colors.current[500], children: selectedItem.name })] }), _jsx(Space, { width: 4 }), _jsx(LakeButton, { mode: "tertiary", size: "small", icon: "more-horizontal-filled", style: styles.button, onPress: () => setMenuOpen(true), ariaLabel: "Open options" }), _jsx(BottomPanel, { visible: menuOpen === true, onPressClose: () => {
120
+ setMenuOpen(false);
121
+ }, children: items.map(item => {
122
+ const isSelected = item.id === selected;
123
+ return (_jsxs(Box, { direction: "row", children: [_jsxs(Pressable, { style: [styles.dropdownItem, isSelected && styles.dropdownItemSelected], onPress: () => {
98
124
  onValueChange(item.id);
99
- setPressed(false);
100
- }, children: [isNotNullish(item.icon) &&
101
- match(item)
102
- .with({ icon: P.nonNullable, activeIcon: P.nonNullable }, () => (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id) === item.id ? selectedItem.activeIcon : item.icon)
103
- .with({ icon: P.nonNullable }, () => item.icon)
104
- .otherwise(() => null), _jsx(Space, { height: 8, width: 12 }), _jsx(LakeText, { color: colors.gray[900], numberOfLines: 1, variant: "regular", style: styles.itemText, children: item.name })] }), (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id) === item.id && (_jsx(Box, { justifyContent: "center", style: { paddingHorizontal: spacings[24], backgroundColor: colors.gray[50] }, children: _jsx(Icon, { size: 16, name: "lake-check", color: colors.positive[500] }) }))] }, item.id))) }), _jsx(Space, { width: 4 }), _jsx(LakeButton, { mode: "tertiary", style: styles.button, size: "small", icon: "more-horizontal-filled", onPress: () => setPressed(true), ariaLabel: "Previous" })] })) : (_jsxs(Box, { direction: "row", children: [_jsx(View, { role: "none", style: [
105
- styles.selectedItemIndicator,
106
- styles.selectedItemDesktop,
107
- {
108
- width: `${(1 / items.length) * 100}%`,
109
- transform: `translateX(${selectedItemIndex * 100}%)`,
110
- },
111
- ] }), items.map(item => (_jsxs(Pressable, { style: styles.itemDesktop, onPress: () => {
112
- onValueChange(item.id);
113
- }, children: [_jsxs(_Fragment, { children: [isNotNullish(item.icon) &&
114
- match(item)
115
- .with({ icon: P.nonNullable, activeIcon: P.nonNullable }, () => (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id) === item.id ? selectedItem.activeIcon : item.icon)
116
- .with({ icon: P.nonNullable }, () => item.icon)
117
- .otherwise(() => null), _jsx(Space, { height: 8, width: 12 })] }), _jsx(LakeText, { color: colors.gray[900], numberOfLines: 1, variant: "regular", style: styles.itemText, children: item.name })] }, item.id)))] })) }));
125
+ setMenuOpen(false);
126
+ }, children: [item.icon != null && (_jsxs(_Fragment, { children: [isSelected && item.activeIcon != null ? item.activeIcon : item.icon, _jsx(Space, { width: 12 })] })), _jsx(Space, { height: 8, width: 12 }), _jsx(LakeText, { color: colors.gray[900], numberOfLines: 1, variant: "regular", children: item.name })] }), isSelected && (_jsx(View, { style: styles.dropdownCheckIconContainer, children: _jsx(Icon, { size: 16, name: "lake-check", color: colors.positive[500] }) }))] }, item.id));
127
+ }) })] }));
118
128
  };