@bacon-pos/ui 0.8.0 → 0.10.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/lib/commonjs/components/EmptyState/EmptyState.js +88 -0
- package/lib/commonjs/components/EmptyState/EmptyState.js.map +1 -0
- package/lib/commonjs/components/EmptyState/EmptyState.stories.js +46 -0
- package/lib/commonjs/components/EmptyState/EmptyState.stories.js.map +1 -0
- package/lib/commonjs/components/EmptyState/EmptyState.test.js +40 -0
- package/lib/commonjs/components/EmptyState/EmptyState.test.js.map +1 -0
- package/lib/commonjs/components/EmptyState/index.js +13 -0
- package/lib/commonjs/components/EmptyState/index.js.map +1 -0
- package/lib/commonjs/components/Icon/icons.generated.js +1 -0
- package/lib/commonjs/components/Icon/icons.generated.js.map +1 -1
- package/lib/commonjs/components/ProgressBar/ProgressBar.js +114 -0
- package/lib/commonjs/components/ProgressBar/ProgressBar.js.map +1 -0
- package/lib/commonjs/components/ProgressBar/ProgressBar.stories.js +93 -0
- package/lib/commonjs/components/ProgressBar/ProgressBar.stories.js.map +1 -0
- package/lib/commonjs/components/ProgressBar/ProgressBar.test.js +70 -0
- package/lib/commonjs/components/ProgressBar/ProgressBar.test.js.map +1 -0
- package/lib/commonjs/components/ProgressBar/index.js +13 -0
- package/lib/commonjs/components/ProgressBar/index.js.map +1 -0
- package/lib/commonjs/components/Sidebar/Sidebar.js +264 -0
- package/lib/commonjs/components/Sidebar/Sidebar.js.map +1 -0
- package/lib/commonjs/components/Sidebar/Sidebar.stories.js +229 -0
- package/lib/commonjs/components/Sidebar/Sidebar.stories.js.map +1 -0
- package/lib/commonjs/components/Sidebar/Sidebar.test.js +131 -0
- package/lib/commonjs/components/Sidebar/Sidebar.test.js.map +1 -0
- package/lib/commonjs/components/Sidebar/index.js +13 -0
- package/lib/commonjs/components/Sidebar/index.js.map +1 -0
- package/lib/commonjs/components/Table/DataTable.js +149 -0
- package/lib/commonjs/components/Table/DataTable.js.map +1 -0
- package/lib/commonjs/components/Table/DataTable.stories.js +214 -0
- package/lib/commonjs/components/Table/DataTable.stories.js.map +1 -0
- package/lib/commonjs/components/Table/DataTable.test.js +143 -0
- package/lib/commonjs/components/Table/DataTable.test.js.map +1 -0
- package/lib/commonjs/components/Table/Table.js +520 -0
- package/lib/commonjs/components/Table/Table.js.map +1 -0
- package/lib/commonjs/components/Table/Table.stories.js +385 -0
- package/lib/commonjs/components/Table/Table.stories.js.map +1 -0
- package/lib/commonjs/components/Table/Table.test.js +278 -0
- package/lib/commonjs/components/Table/Table.test.js.map +1 -0
- package/lib/commonjs/components/Table/index.js +20 -0
- package/lib/commonjs/components/Table/index.js.map +1 -0
- package/lib/commonjs/components/brand/BaconLogo.js +29 -0
- package/lib/commonjs/components/brand/BaconLogo.js.map +1 -0
- package/lib/commonjs/components/brand/index.js +7 -0
- package/lib/commonjs/components/brand/index.js.map +1 -1
- package/lib/commonjs/components/index.js +60 -16
- package/lib/commonjs/components/index.js.map +1 -1
- package/lib/module/components/EmptyState/EmptyState.js +84 -0
- package/lib/module/components/EmptyState/EmptyState.js.map +1 -0
- package/lib/module/components/EmptyState/EmptyState.stories.js +42 -0
- package/lib/module/components/EmptyState/EmptyState.stories.js.map +1 -0
- package/lib/module/components/EmptyState/EmptyState.test.js +40 -0
- package/lib/module/components/EmptyState/EmptyState.test.js.map +1 -0
- package/lib/module/components/EmptyState/index.js +4 -0
- package/lib/module/components/EmptyState/index.js.map +1 -0
- package/lib/module/components/Icon/icons.generated.js +1 -0
- package/lib/module/components/Icon/icons.generated.js.map +1 -1
- package/lib/module/components/ProgressBar/ProgressBar.js +110 -0
- package/lib/module/components/ProgressBar/ProgressBar.js.map +1 -0
- package/lib/module/components/ProgressBar/ProgressBar.stories.js +89 -0
- package/lib/module/components/ProgressBar/ProgressBar.stories.js.map +1 -0
- package/lib/module/components/ProgressBar/ProgressBar.test.js +70 -0
- package/lib/module/components/ProgressBar/ProgressBar.test.js.map +1 -0
- package/lib/module/components/ProgressBar/index.js +4 -0
- package/lib/module/components/ProgressBar/index.js.map +1 -0
- package/lib/module/components/Sidebar/Sidebar.js +260 -0
- package/lib/module/components/Sidebar/Sidebar.js.map +1 -0
- package/lib/module/components/Sidebar/Sidebar.stories.js +225 -0
- package/lib/module/components/Sidebar/Sidebar.stories.js.map +1 -0
- package/lib/module/components/Sidebar/Sidebar.test.js +131 -0
- package/lib/module/components/Sidebar/Sidebar.test.js.map +1 -0
- package/lib/module/components/Sidebar/index.js +4 -0
- package/lib/module/components/Sidebar/index.js.map +1 -0
- package/lib/module/components/Table/DataTable.js +145 -0
- package/lib/module/components/Table/DataTable.js.map +1 -0
- package/lib/module/components/Table/DataTable.stories.js +210 -0
- package/lib/module/components/Table/DataTable.stories.js.map +1 -0
- package/lib/module/components/Table/DataTable.test.js +143 -0
- package/lib/module/components/Table/DataTable.test.js.map +1 -0
- package/lib/module/components/Table/Table.js +516 -0
- package/lib/module/components/Table/Table.js.map +1 -0
- package/lib/module/components/Table/Table.stories.js +381 -0
- package/lib/module/components/Table/Table.stories.js.map +1 -0
- package/lib/module/components/Table/Table.test.js +278 -0
- package/lib/module/components/Table/Table.test.js.map +1 -0
- package/lib/module/components/Table/index.js +5 -0
- package/lib/module/components/Table/index.js.map +1 -0
- package/lib/module/components/brand/BaconLogo.js +25 -0
- package/lib/module/components/brand/BaconLogo.js.map +1 -0
- package/lib/module/components/brand/index.js +1 -0
- package/lib/module/components/brand/index.js.map +1 -1
- package/lib/module/components/index.js +4 -0
- package/lib/module/components/index.js.map +1 -1
- package/lib/typescript/commonjs/components/EmptyState/EmptyState.d.ts +30 -0
- package/lib/typescript/commonjs/components/EmptyState/EmptyState.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/EmptyState/index.d.ts +3 -0
- package/lib/typescript/commonjs/components/EmptyState/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/Icon/icons.generated.d.ts +1 -0
- package/lib/typescript/commonjs/components/Icon/icons.generated.d.ts.map +1 -1
- package/lib/typescript/commonjs/components/ProgressBar/ProgressBar.d.ts +37 -0
- package/lib/typescript/commonjs/components/ProgressBar/ProgressBar.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/ProgressBar/index.d.ts +3 -0
- package/lib/typescript/commonjs/components/ProgressBar/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/Sidebar/Sidebar.d.ts +57 -0
- package/lib/typescript/commonjs/components/Sidebar/Sidebar.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/Sidebar/index.d.ts +3 -0
- package/lib/typescript/commonjs/components/Sidebar/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/Table/DataTable.d.ts +53 -0
- package/lib/typescript/commonjs/components/Table/DataTable.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/Table/Table.d.ts +85 -0
- package/lib/typescript/commonjs/components/Table/Table.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/Table/index.d.ts +5 -0
- package/lib/typescript/commonjs/components/Table/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/brand/BaconLogo.d.ts +13 -0
- package/lib/typescript/commonjs/components/brand/BaconLogo.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/brand/index.d.ts +1 -0
- package/lib/typescript/commonjs/components/brand/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/components/index.d.ts +4 -0
- package/lib/typescript/commonjs/components/index.d.ts.map +1 -1
- package/lib/typescript/module/components/EmptyState/EmptyState.d.ts +30 -0
- package/lib/typescript/module/components/EmptyState/EmptyState.d.ts.map +1 -0
- package/lib/typescript/module/components/EmptyState/index.d.ts +3 -0
- package/lib/typescript/module/components/EmptyState/index.d.ts.map +1 -0
- package/lib/typescript/module/components/Icon/icons.generated.d.ts +1 -0
- package/lib/typescript/module/components/Icon/icons.generated.d.ts.map +1 -1
- package/lib/typescript/module/components/ProgressBar/ProgressBar.d.ts +37 -0
- package/lib/typescript/module/components/ProgressBar/ProgressBar.d.ts.map +1 -0
- package/lib/typescript/module/components/ProgressBar/index.d.ts +3 -0
- package/lib/typescript/module/components/ProgressBar/index.d.ts.map +1 -0
- package/lib/typescript/module/components/Sidebar/Sidebar.d.ts +57 -0
- package/lib/typescript/module/components/Sidebar/Sidebar.d.ts.map +1 -0
- package/lib/typescript/module/components/Sidebar/index.d.ts +3 -0
- package/lib/typescript/module/components/Sidebar/index.d.ts.map +1 -0
- package/lib/typescript/module/components/Table/DataTable.d.ts +53 -0
- package/lib/typescript/module/components/Table/DataTable.d.ts.map +1 -0
- package/lib/typescript/module/components/Table/Table.d.ts +85 -0
- package/lib/typescript/module/components/Table/Table.d.ts.map +1 -0
- package/lib/typescript/module/components/Table/index.d.ts +5 -0
- package/lib/typescript/module/components/Table/index.d.ts.map +1 -0
- package/lib/typescript/module/components/brand/BaconLogo.d.ts +13 -0
- package/lib/typescript/module/components/brand/BaconLogo.d.ts.map +1 -0
- package/lib/typescript/module/components/brand/index.d.ts +1 -0
- package/lib/typescript/module/components/brand/index.d.ts.map +1 -1
- package/lib/typescript/module/components/index.d.ts +4 -0
- package/lib/typescript/module/components/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/EmptyState/EmptyState.tsx +91 -0
- package/src/components/EmptyState/index.ts +2 -0
- package/src/components/Icon/icons.generated.ts +1 -0
- package/src/components/ProgressBar/ProgressBar.tsx +135 -0
- package/src/components/ProgressBar/index.ts +2 -0
- package/src/components/Sidebar/Sidebar.tsx +320 -0
- package/src/components/Sidebar/index.ts +2 -0
- package/src/components/Table/DataTable.tsx +173 -0
- package/src/components/Table/Table.tsx +556 -0
- package/src/components/Table/index.ts +10 -0
- package/src/components/brand/BaconLogo.tsx +25 -0
- package/src/components/brand/index.ts +1 -0
- package/src/components/index.ts +4 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Animated,
|
|
4
|
+
Easing,
|
|
5
|
+
Pressable,
|
|
6
|
+
View,
|
|
7
|
+
type StyleProp,
|
|
8
|
+
type ViewStyle,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
import { useTheme } from '../../theme';
|
|
11
|
+
import { BaconLogo } from '../brand/BaconLogo';
|
|
12
|
+
import { Icon } from '../Icon/Icon';
|
|
13
|
+
import type { IconName } from '../Icon/icons.generated';
|
|
14
|
+
import { IconButton } from '../IconButton/IconButton';
|
|
15
|
+
import { Text } from '../Text/Text';
|
|
16
|
+
import { ScrollArea } from '../_internal/ScrollArea';
|
|
17
|
+
|
|
18
|
+
export interface SidebarItem {
|
|
19
|
+
/** Stable key; also the active identifier. */
|
|
20
|
+
key: string;
|
|
21
|
+
/** Button text (hidden when collapsed). */
|
|
22
|
+
label: string;
|
|
23
|
+
/** Leading icon. When omitted, expanded reserves the icon space and collapsed shows the label's initial. */
|
|
24
|
+
icon?: IconName;
|
|
25
|
+
/** Optional icon shown when the item is active (e.g. a filled variant). */
|
|
26
|
+
activeIcon?: IconName;
|
|
27
|
+
/** Press handler. */
|
|
28
|
+
onPress?: () => void;
|
|
29
|
+
/** Dim and block the item. */
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SidebarProps {
|
|
34
|
+
/** Navigation items — rendered top-to-bottom. Defaults to none. */
|
|
35
|
+
items?: SidebarItem[];
|
|
36
|
+
/** Active item key (controlled). */
|
|
37
|
+
activeKey?: string;
|
|
38
|
+
/** Active item key for the uncontrolled case. */
|
|
39
|
+
defaultActiveKey?: string;
|
|
40
|
+
/** Called with the pressed item's key. */
|
|
41
|
+
onChange?: (key: string) => void;
|
|
42
|
+
|
|
43
|
+
/** Collapsed (icons only) state — controlled. */
|
|
44
|
+
collapsed?: boolean;
|
|
45
|
+
/** Initial collapsed state (uncontrolled). Defaults to false. */
|
|
46
|
+
defaultCollapsed?: boolean;
|
|
47
|
+
/** Called when the collapse state changes. */
|
|
48
|
+
onCollapsedChange?: (collapsed: boolean) => void;
|
|
49
|
+
/** Show the built-in expand/collapse toggle. Defaults to true. */
|
|
50
|
+
showToggle?: boolean;
|
|
51
|
+
|
|
52
|
+
/** Logo shown at the top. Defaults to the Bacon mark. */
|
|
53
|
+
logo?: ReactNode;
|
|
54
|
+
/** Bottom supporting text (e.g. a version). Defaults to none. */
|
|
55
|
+
footer?: string;
|
|
56
|
+
|
|
57
|
+
/** Expanded width. Defaults to 284 (Figma). */
|
|
58
|
+
expandedWidth?: number;
|
|
59
|
+
/** Collapsed width. Defaults to 122 (Figma). */
|
|
60
|
+
collapsedWidth?: number;
|
|
61
|
+
/** Scroll indicator color. Defaults to a subtle neutral. */
|
|
62
|
+
scrollbarColor?: string;
|
|
63
|
+
/** Override the container style. */
|
|
64
|
+
style?: StyleProp<ViewStyle>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const ITEM_HEIGHT = 74;
|
|
68
|
+
const ITEM_GAP = 16;
|
|
69
|
+
const PADDING = 24;
|
|
70
|
+
const TOGGLE_SIZE = 40; // IconButton size "sm"
|
|
71
|
+
|
|
72
|
+
function SidebarButton({
|
|
73
|
+
item,
|
|
74
|
+
active,
|
|
75
|
+
collapsed,
|
|
76
|
+
onPress,
|
|
77
|
+
}: {
|
|
78
|
+
item: SidebarItem;
|
|
79
|
+
active: boolean;
|
|
80
|
+
collapsed: boolean;
|
|
81
|
+
onPress: () => void;
|
|
82
|
+
}) {
|
|
83
|
+
const theme = useTheme();
|
|
84
|
+
const [hovered, setHovered] = useState(false);
|
|
85
|
+
const disabled = item.disabled;
|
|
86
|
+
|
|
87
|
+
const backgroundColor = active
|
|
88
|
+
? theme.colors.action.secondary
|
|
89
|
+
: hovered && !disabled
|
|
90
|
+
? theme.colors.background.muted
|
|
91
|
+
: 'transparent';
|
|
92
|
+
const contentColor = active
|
|
93
|
+
? theme.colors.action.secondaryForeground
|
|
94
|
+
: theme.colors.text.secondary;
|
|
95
|
+
const iconColor = active ? theme.colors.action.secondaryForeground : 'secondary';
|
|
96
|
+
const iconName = active && item.activeIcon ? item.activeIcon : item.icon;
|
|
97
|
+
const initial = item.label.trim().charAt(0).toUpperCase();
|
|
98
|
+
|
|
99
|
+
// Leading slot is always a fixed 24×24 box (flexShrink: 0) so the icon never
|
|
100
|
+
// shrinks next to a long label and labels stay aligned with or without an icon.
|
|
101
|
+
// No icon → reserve the space when expanded; show the initial when collapsed.
|
|
102
|
+
const leadingBox: ViewStyle = {
|
|
103
|
+
width: 24,
|
|
104
|
+
height: 24,
|
|
105
|
+
flexShrink: 0,
|
|
106
|
+
alignItems: 'center',
|
|
107
|
+
justifyContent: 'center',
|
|
108
|
+
};
|
|
109
|
+
const leading = (
|
|
110
|
+
<View style={leadingBox}>
|
|
111
|
+
{iconName ? (
|
|
112
|
+
<Icon name={iconName} size={24} color={iconColor} />
|
|
113
|
+
) : collapsed ? (
|
|
114
|
+
<Text variant="bodyLarge" weight="medium" color={contentColor}>
|
|
115
|
+
{initial}
|
|
116
|
+
</Text>
|
|
117
|
+
) : null}
|
|
118
|
+
</View>
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<Pressable
|
|
123
|
+
onPress={disabled ? undefined : onPress}
|
|
124
|
+
onHoverIn={() => setHovered(true)}
|
|
125
|
+
onHoverOut={() => setHovered(false)}
|
|
126
|
+
disabled={disabled}
|
|
127
|
+
accessibilityRole="tab"
|
|
128
|
+
accessibilityState={{ selected: active, disabled }}
|
|
129
|
+
accessibilityLabel={item.label}
|
|
130
|
+
style={{
|
|
131
|
+
flexDirection: 'row',
|
|
132
|
+
alignItems: 'center',
|
|
133
|
+
justifyContent: collapsed ? 'center' : 'flex-start',
|
|
134
|
+
gap: 10,
|
|
135
|
+
height: ITEM_HEIGHT,
|
|
136
|
+
paddingHorizontal: PADDING,
|
|
137
|
+
borderRadius: theme.radii.pill,
|
|
138
|
+
borderWidth: active ? 1 : 0,
|
|
139
|
+
borderColor: active ? theme.colors.border.default : 'transparent',
|
|
140
|
+
backgroundColor,
|
|
141
|
+
opacity: disabled ? 0.5 : 1,
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
{leading}
|
|
145
|
+
{!collapsed ? (
|
|
146
|
+
<Text
|
|
147
|
+
variant="bodyLarge"
|
|
148
|
+
weight="medium"
|
|
149
|
+
color={contentColor}
|
|
150
|
+
numberOfLines={1}
|
|
151
|
+
ellipsizeMode="tail"
|
|
152
|
+
style={{ flexShrink: 1, minWidth: 0 }}
|
|
153
|
+
>
|
|
154
|
+
{item.label}
|
|
155
|
+
</Text>
|
|
156
|
+
) : null}
|
|
157
|
+
</Pressable>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Sidebar — the app's main navigation drawer (Figma "Sidebar"). Expands/collapses
|
|
163
|
+
* between a labelled and an icon-only rail (animated width). The logo defaults to
|
|
164
|
+
* the Bacon mark (override via `logo`); an optional `footer` shows supporting text
|
|
165
|
+
* at the bottom. Items are dynamic buttons (icon + label via props); the list
|
|
166
|
+
* scrolls with an elegant auto-hiding indicator when it overflows, leaving the
|
|
167
|
+
* footer pinned. Buttons have hover and active states to mark the current section.
|
|
168
|
+
*/
|
|
169
|
+
export function Sidebar({
|
|
170
|
+
items = [],
|
|
171
|
+
activeKey,
|
|
172
|
+
defaultActiveKey,
|
|
173
|
+
onChange,
|
|
174
|
+
collapsed: collapsedProp,
|
|
175
|
+
defaultCollapsed = false,
|
|
176
|
+
onCollapsedChange,
|
|
177
|
+
showToggle = true,
|
|
178
|
+
logo,
|
|
179
|
+
footer,
|
|
180
|
+
expandedWidth = 284,
|
|
181
|
+
collapsedWidth = 122,
|
|
182
|
+
scrollbarColor,
|
|
183
|
+
style,
|
|
184
|
+
}: SidebarProps) {
|
|
185
|
+
const theme = useTheme();
|
|
186
|
+
|
|
187
|
+
const activeControlled = activeKey !== undefined;
|
|
188
|
+
const [activeInternal, setActiveInternal] = useState(defaultActiveKey);
|
|
189
|
+
const active = activeControlled ? activeKey : activeInternal;
|
|
190
|
+
|
|
191
|
+
const collapseControlled = collapsedProp !== undefined;
|
|
192
|
+
const [collapsedInternal, setCollapsedInternal] = useState(defaultCollapsed);
|
|
193
|
+
const collapsed = collapseControlled ? collapsedProp : collapsedInternal;
|
|
194
|
+
|
|
195
|
+
const width = useRef(new Animated.Value(collapsed ? collapsedWidth : expandedWidth)).current;
|
|
196
|
+
// `visualCollapsed` drives the layout (labels, alignment, footer). It lags on
|
|
197
|
+
// collapse — labels stay mounted (clipped by overflow) until the width finishes
|
|
198
|
+
// shrinking — and leads on expand, so both directions animate without a jump.
|
|
199
|
+
const [visualCollapsed, setVisualCollapsed] = useState(collapsed);
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
if (!collapsed) setVisualCollapsed(false);
|
|
202
|
+
const anim = Animated.timing(width, {
|
|
203
|
+
toValue: collapsed ? collapsedWidth : expandedWidth,
|
|
204
|
+
duration: 220,
|
|
205
|
+
easing: Easing.out(Easing.ease),
|
|
206
|
+
useNativeDriver: false, // width
|
|
207
|
+
});
|
|
208
|
+
anim.start(({ finished }) => {
|
|
209
|
+
if (finished && collapsed) setVisualCollapsed(true);
|
|
210
|
+
});
|
|
211
|
+
return () => anim.stop();
|
|
212
|
+
}, [collapsed, collapsedWidth, expandedWidth, width]);
|
|
213
|
+
|
|
214
|
+
// The toggle arrow spins in place (0° → 180°) instead of moving, so collapsing
|
|
215
|
+
// never nudges it. Driven by intent (`collapsed`), in sync with the width.
|
|
216
|
+
const arrowRot = useRef(new Animated.Value(collapsed ? 1 : 0)).current;
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
const anim = Animated.timing(arrowRot, {
|
|
219
|
+
toValue: collapsed ? 1 : 0,
|
|
220
|
+
duration: 220,
|
|
221
|
+
easing: Easing.out(Easing.ease),
|
|
222
|
+
useNativeDriver: true,
|
|
223
|
+
});
|
|
224
|
+
anim.start();
|
|
225
|
+
return () => anim.stop();
|
|
226
|
+
}, [collapsed, arrowRot]);
|
|
227
|
+
const arrowRotate = arrowRot.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '180deg'] });
|
|
228
|
+
|
|
229
|
+
// Glide the toggle from flush-left (expanded) to centered on the icon axis
|
|
230
|
+
// (collapsed), tracking the width so it never jumps.
|
|
231
|
+
const toggleTranslateX = width.interpolate({
|
|
232
|
+
inputRange: [collapsedWidth, expandedWidth],
|
|
233
|
+
outputRange: [Math.max(0, (collapsedWidth - 2 * PADDING - TOGGLE_SIZE) / 2), 0],
|
|
234
|
+
extrapolate: 'clamp',
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const selectItem = (item: SidebarItem) => {
|
|
238
|
+
if (!activeControlled) setActiveInternal(item.key);
|
|
239
|
+
item.onPress?.();
|
|
240
|
+
onChange?.(item.key);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const toggleCollapsed = () => {
|
|
244
|
+
const next = !collapsed;
|
|
245
|
+
if (!collapseControlled) setCollapsedInternal(next);
|
|
246
|
+
onCollapsedChange?.(next);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const logoNode = logo ?? <BaconLogo height={visualCollapsed ? 36 : 40} />;
|
|
250
|
+
const toggle = showToggle ? (
|
|
251
|
+
<Animated.View style={{ transform: [{ translateX: toggleTranslateX }] }}>
|
|
252
|
+
<Animated.View style={{ transform: [{ rotate: arrowRotate }] }}>
|
|
253
|
+
<IconButton
|
|
254
|
+
icon="arrow-left"
|
|
255
|
+
accessibilityLabel={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
256
|
+
variant="ghost"
|
|
257
|
+
size="sm"
|
|
258
|
+
onPress={toggleCollapsed}
|
|
259
|
+
/>
|
|
260
|
+
</Animated.View>
|
|
261
|
+
</Animated.View>
|
|
262
|
+
) : null;
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<Animated.View
|
|
266
|
+
style={[
|
|
267
|
+
{
|
|
268
|
+
width,
|
|
269
|
+
height: '100%',
|
|
270
|
+
backgroundColor: theme.colors.background.default,
|
|
271
|
+
paddingTop: 48,
|
|
272
|
+
paddingBottom: PADDING,
|
|
273
|
+
paddingHorizontal: PADDING,
|
|
274
|
+
overflow: 'hidden',
|
|
275
|
+
},
|
|
276
|
+
style,
|
|
277
|
+
]}
|
|
278
|
+
>
|
|
279
|
+
{/* Logo */}
|
|
280
|
+
<View
|
|
281
|
+
style={{
|
|
282
|
+
minHeight: 56,
|
|
283
|
+
alignItems: visualCollapsed ? 'center' : 'flex-start',
|
|
284
|
+
justifyContent: 'center',
|
|
285
|
+
}}
|
|
286
|
+
>
|
|
287
|
+
{logoNode}
|
|
288
|
+
</View>
|
|
289
|
+
|
|
290
|
+
{/* Tabs (scrolls when overflowing; footer stays pinned below) */}
|
|
291
|
+
<ScrollArea
|
|
292
|
+
style={{ flex: 1, marginTop: 32 }}
|
|
293
|
+
indicatorColor={scrollbarColor ?? theme.colors.border.strong}
|
|
294
|
+
contentContainerStyle={{ gap: ITEM_GAP, paddingBottom: 8 }}
|
|
295
|
+
>
|
|
296
|
+
{items.map((item) => (
|
|
297
|
+
<SidebarButton
|
|
298
|
+
key={item.key}
|
|
299
|
+
item={item}
|
|
300
|
+
active={item.key === active}
|
|
301
|
+
collapsed={visualCollapsed}
|
|
302
|
+
onPress={() => selectItem(item)}
|
|
303
|
+
/>
|
|
304
|
+
))}
|
|
305
|
+
</ScrollArea>
|
|
306
|
+
|
|
307
|
+
{/* Bottom: supporting footer + fixed collapse/expand toggle */}
|
|
308
|
+
<View style={{ marginTop: 28, gap: 16 }}>
|
|
309
|
+
{footer && !visualCollapsed ? (
|
|
310
|
+
<Text variant="bodySmall" color="tertiary" numberOfLines={1} style={{ marginLeft: 8 }}>
|
|
311
|
+
{footer}
|
|
312
|
+
</Text>
|
|
313
|
+
) : null}
|
|
314
|
+
{toggle ? (
|
|
315
|
+
<View style={{ alignItems: 'flex-start' }}>{toggle}</View>
|
|
316
|
+
) : null}
|
|
317
|
+
</View>
|
|
318
|
+
</Animated.View>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { View, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { useTheme } from '../../theme';
|
|
4
|
+
import { Button } from '../Button/Button';
|
|
5
|
+
import { ChipGroup } from '../Chip/ChipGroup';
|
|
6
|
+
import { EmptyState, type EmptyStateProps } from '../EmptyState/EmptyState';
|
|
7
|
+
import { Icon } from '../Icon/Icon';
|
|
8
|
+
import type { IconName } from '../Icon/icons.generated';
|
|
9
|
+
import { IconButton } from '../IconButton/IconButton';
|
|
10
|
+
import { Input } from '../Input/Input';
|
|
11
|
+
import { Text } from '../Text/Text';
|
|
12
|
+
import { Table, type TableProps } from './Table';
|
|
13
|
+
|
|
14
|
+
export interface DataTableTab {
|
|
15
|
+
value: string;
|
|
16
|
+
label: string;
|
|
17
|
+
icon?: IconName;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface DataTablePagination {
|
|
21
|
+
/** 1-based current page. */
|
|
22
|
+
page: number;
|
|
23
|
+
pageSize: number;
|
|
24
|
+
/** Total row count (across pages). */
|
|
25
|
+
total: number;
|
|
26
|
+
onPageChange: (page: number) => void;
|
|
27
|
+
/** Customize the range label. Defaults to `${from}–${to} of ${total}`. */
|
|
28
|
+
formatRange?: (from: number, to: number, total: number) => string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DataTableProps<T> extends Omit<TableProps<T>, 'empty'> {
|
|
32
|
+
/** Tabs above the toolbar (reuses `ChipGroup`). */
|
|
33
|
+
tabs?: DataTableTab[];
|
|
34
|
+
/** Active tab (controlled). */
|
|
35
|
+
activeTab?: string;
|
|
36
|
+
defaultTab?: string;
|
|
37
|
+
onTabChange?: (value: string) => void;
|
|
38
|
+
|
|
39
|
+
/** Show the search field. */
|
|
40
|
+
searchable?: boolean;
|
|
41
|
+
searchValue?: string;
|
|
42
|
+
onSearchChange?: (value: string) => void;
|
|
43
|
+
searchPlaceholder?: string;
|
|
44
|
+
|
|
45
|
+
/** Show a create button on the right of the toolbar. */
|
|
46
|
+
createLabel?: string;
|
|
47
|
+
onCreate?: () => void;
|
|
48
|
+
createIcon?: IconName;
|
|
49
|
+
|
|
50
|
+
/** Structured empty state (alternative to Table's `empty` node). */
|
|
51
|
+
emptyState?: EmptyStateProps;
|
|
52
|
+
/** Raw empty node (wins over `emptyState`). */
|
|
53
|
+
empty?: React.ReactNode;
|
|
54
|
+
|
|
55
|
+
/** Pagination controls below the table. */
|
|
56
|
+
pagination?: DataTablePagination;
|
|
57
|
+
|
|
58
|
+
/** Gap between toolbar sections. Defaults to 16. */
|
|
59
|
+
gap?: number;
|
|
60
|
+
containerStyle?: StyleProp<ViewStyle>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* DataTable — the turnkey management table (Figma): optional tabs (`ChipGroup`),
|
|
65
|
+
* a search + create toolbar, the `Table` body, a structured `EmptyState`, and
|
|
66
|
+
* optional pagination — assembled with consistent spacing. For bespoke layouts,
|
|
67
|
+
* compose `Table` + `EmptyState` yourself.
|
|
68
|
+
*/
|
|
69
|
+
export function DataTable<T>({
|
|
70
|
+
tabs,
|
|
71
|
+
activeTab,
|
|
72
|
+
defaultTab,
|
|
73
|
+
onTabChange,
|
|
74
|
+
searchable = false,
|
|
75
|
+
searchValue,
|
|
76
|
+
onSearchChange,
|
|
77
|
+
searchPlaceholder = 'Search…',
|
|
78
|
+
createLabel,
|
|
79
|
+
onCreate,
|
|
80
|
+
createIcon = 'plus',
|
|
81
|
+
emptyState,
|
|
82
|
+
empty,
|
|
83
|
+
pagination,
|
|
84
|
+
gap = 16,
|
|
85
|
+
containerStyle,
|
|
86
|
+
...tableProps
|
|
87
|
+
}: DataTableProps<T>) {
|
|
88
|
+
const theme = useTheme();
|
|
89
|
+
|
|
90
|
+
const tabControlled = activeTab !== undefined;
|
|
91
|
+
const [tabInternal, setTabInternal] = useState<string | null>(defaultTab ?? tabs?.[0]?.value ?? null);
|
|
92
|
+
const currentTab = tabControlled ? activeTab : tabInternal;
|
|
93
|
+
const setTab = (value: string | null) => {
|
|
94
|
+
if (value == null) return; // tabs are not deselectable
|
|
95
|
+
if (!tabControlled) setTabInternal(value);
|
|
96
|
+
onTabChange?.(value);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const emptyNode = empty ?? (emptyState ? <EmptyState {...emptyState} /> : undefined);
|
|
100
|
+
|
|
101
|
+
const showToolbar = searchable || !!createLabel;
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<View style={[{ gap }, containerStyle]}>
|
|
105
|
+
{tabs && tabs.length > 0 ? (
|
|
106
|
+
<ChipGroup
|
|
107
|
+
tone="dark"
|
|
108
|
+
options={tabs.map((t) => ({ value: t.value, label: t.label, icon: t.icon }))}
|
|
109
|
+
value={currentTab}
|
|
110
|
+
onChange={setTab}
|
|
111
|
+
/>
|
|
112
|
+
) : null}
|
|
113
|
+
|
|
114
|
+
{showToolbar ? (
|
|
115
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
|
|
116
|
+
{searchable ? (
|
|
117
|
+
<View style={{ flex: 1, minWidth: 220 }}>
|
|
118
|
+
<Input
|
|
119
|
+
value={searchValue}
|
|
120
|
+
onChangeText={onSearchChange}
|
|
121
|
+
placeholder={searchPlaceholder}
|
|
122
|
+
leftIcon={<Icon name="search" size={24} color="tertiary" />}
|
|
123
|
+
/>
|
|
124
|
+
</View>
|
|
125
|
+
) : null}
|
|
126
|
+
{createLabel ? (
|
|
127
|
+
<Button
|
|
128
|
+
label={createLabel}
|
|
129
|
+
variant="inverse"
|
|
130
|
+
size="md"
|
|
131
|
+
onPress={onCreate}
|
|
132
|
+
leftIcon={<Icon name={createIcon} size={24} color={theme.colors.action.inverseForeground} />}
|
|
133
|
+
/>
|
|
134
|
+
) : null}
|
|
135
|
+
</View>
|
|
136
|
+
) : null}
|
|
137
|
+
|
|
138
|
+
<Table {...tableProps} empty={emptyNode} />
|
|
139
|
+
|
|
140
|
+
{pagination ? <Paginator {...pagination} /> : null}
|
|
141
|
+
</View>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function Paginator({ page, pageSize, total, onPageChange, formatRange }: DataTablePagination) {
|
|
146
|
+
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
|
147
|
+
const from = total === 0 ? 0 : (page - 1) * pageSize + 1;
|
|
148
|
+
const to = Math.min(page * pageSize, total);
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-end', gap: 12 }}>
|
|
152
|
+
<Text variant="bodySmall" color="secondary">
|
|
153
|
+
{formatRange ? formatRange(from, to, total) : `${from}–${to} of ${total}`}
|
|
154
|
+
</Text>
|
|
155
|
+
<IconButton
|
|
156
|
+
icon="arrow-left"
|
|
157
|
+
accessibilityLabel="Previous page"
|
|
158
|
+
variant="secondary"
|
|
159
|
+
size="sm"
|
|
160
|
+
disabled={page <= 1}
|
|
161
|
+
onPress={() => onPageChange(page - 1)}
|
|
162
|
+
/>
|
|
163
|
+
<IconButton
|
|
164
|
+
icon="arrow-right"
|
|
165
|
+
accessibilityLabel="Next page"
|
|
166
|
+
variant="secondary"
|
|
167
|
+
size="sm"
|
|
168
|
+
disabled={page >= totalPages}
|
|
169
|
+
onPress={() => onPageChange(page + 1)}
|
|
170
|
+
/>
|
|
171
|
+
</View>
|
|
172
|
+
);
|
|
173
|
+
}
|