@bacon-pos/ui 0.10.0 → 0.15.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.
Files changed (213) hide show
  1. package/lib/commonjs/components/Alert/Alert.js +137 -0
  2. package/lib/commonjs/components/Alert/Alert.js.map +1 -0
  3. package/lib/commonjs/components/Alert/Alert.stories.js +104 -0
  4. package/lib/commonjs/components/Alert/Alert.stories.js.map +1 -0
  5. package/lib/commonjs/components/Alert/Alert.test.js +62 -0
  6. package/lib/commonjs/components/Alert/Alert.test.js.map +1 -0
  7. package/lib/commonjs/components/Alert/index.js +13 -0
  8. package/lib/commonjs/components/Alert/index.js.map +1 -0
  9. package/lib/commonjs/components/CodeInput/CodeInput.js +28 -9
  10. package/lib/commonjs/components/CodeInput/CodeInput.js.map +1 -1
  11. package/lib/commonjs/components/CodeInput/CodeInput.stories.js +28 -1
  12. package/lib/commonjs/components/CodeInput/CodeInput.stories.js.map +1 -1
  13. package/lib/commonjs/components/CodeInput/CodeInput.test.js +16 -0
  14. package/lib/commonjs/components/CodeInput/CodeInput.test.js.map +1 -1
  15. package/lib/commonjs/components/Dialog/Dialog.js +116 -71
  16. package/lib/commonjs/components/Dialog/Dialog.js.map +1 -1
  17. package/lib/commonjs/components/Dialog/StepDialog.js +111 -0
  18. package/lib/commonjs/components/Dialog/StepDialog.js.map +1 -0
  19. package/lib/commonjs/components/Dialog/StepDialog.stories.js +157 -0
  20. package/lib/commonjs/components/Dialog/StepDialog.stories.js.map +1 -0
  21. package/lib/commonjs/components/Dialog/StepDialog.test.js +118 -0
  22. package/lib/commonjs/components/Dialog/StepDialog.test.js.map +1 -0
  23. package/lib/commonjs/components/Dialog/index.js +7 -0
  24. package/lib/commonjs/components/Dialog/index.js.map +1 -1
  25. package/lib/commonjs/components/Icon/icons.generated.js +9 -1
  26. package/lib/commonjs/components/Icon/icons.generated.js.map +1 -1
  27. package/lib/commonjs/components/List/List.js +45 -0
  28. package/lib/commonjs/components/List/List.js.map +1 -0
  29. package/lib/commonjs/components/List/List.stories.js +215 -0
  30. package/lib/commonjs/components/List/List.stories.js.map +1 -0
  31. package/lib/commonjs/components/List/List.test.js +94 -0
  32. package/lib/commonjs/components/List/List.test.js.map +1 -0
  33. package/lib/commonjs/components/List/ListItem.js +210 -0
  34. package/lib/commonjs/components/List/ListItem.js.map +1 -0
  35. package/lib/commonjs/components/List/index.js +20 -0
  36. package/lib/commonjs/components/List/index.js.map +1 -0
  37. package/lib/commonjs/components/Menu/Menu.js +11 -3
  38. package/lib/commonjs/components/Menu/Menu.js.map +1 -1
  39. package/lib/commonjs/components/ProgressBar/ProgressBar.js +4 -3
  40. package/lib/commonjs/components/ProgressBar/ProgressBar.js.map +1 -1
  41. package/lib/commonjs/components/Sidebar/Sidebar.js +20 -3
  42. package/lib/commonjs/components/Sidebar/Sidebar.js.map +1 -1
  43. package/lib/commonjs/components/Sidebar/Sidebar.stories.js +17 -1
  44. package/lib/commonjs/components/Sidebar/Sidebar.stories.js.map +1 -1
  45. package/lib/commonjs/components/StatTile/StatTile.js +94 -0
  46. package/lib/commonjs/components/StatTile/StatTile.js.map +1 -0
  47. package/lib/commonjs/components/StatTile/StatTile.stories.js +100 -0
  48. package/lib/commonjs/components/StatTile/StatTile.stories.js.map +1 -0
  49. package/lib/commonjs/components/StatTile/StatTile.test.js +49 -0
  50. package/lib/commonjs/components/StatTile/StatTile.test.js.map +1 -0
  51. package/lib/commonjs/components/StatTile/index.js +13 -0
  52. package/lib/commonjs/components/StatTile/index.js.map +1 -0
  53. package/lib/commonjs/components/Table/Table.js +69 -6
  54. package/lib/commonjs/components/Table/Table.js.map +1 -1
  55. package/lib/commonjs/components/Table/Table.stories.js +118 -1
  56. package/lib/commonjs/components/Table/Table.stories.js.map +1 -1
  57. package/lib/commonjs/components/Table/Table.test.js +58 -0
  58. package/lib/commonjs/components/Table/Table.test.js.map +1 -1
  59. package/lib/commonjs/components/_internal/ModalHostContext.js +21 -0
  60. package/lib/commonjs/components/_internal/ModalHostContext.js.map +1 -0
  61. package/lib/commonjs/components/index.js +64 -31
  62. package/lib/commonjs/components/index.js.map +1 -1
  63. package/lib/module/components/Alert/Alert.js +133 -0
  64. package/lib/module/components/Alert/Alert.js.map +1 -0
  65. package/lib/module/components/Alert/Alert.stories.js +100 -0
  66. package/lib/module/components/Alert/Alert.stories.js.map +1 -0
  67. package/lib/module/components/Alert/Alert.test.js +62 -0
  68. package/lib/module/components/Alert/Alert.test.js.map +1 -0
  69. package/lib/module/components/Alert/index.js +4 -0
  70. package/lib/module/components/Alert/index.js.map +1 -0
  71. package/lib/module/components/CodeInput/CodeInput.js +28 -9
  72. package/lib/module/components/CodeInput/CodeInput.js.map +1 -1
  73. package/lib/module/components/CodeInput/CodeInput.stories.js +27 -0
  74. package/lib/module/components/CodeInput/CodeInput.stories.js.map +1 -1
  75. package/lib/module/components/CodeInput/CodeInput.test.js +16 -0
  76. package/lib/module/components/CodeInput/CodeInput.test.js.map +1 -1
  77. package/lib/module/components/Dialog/Dialog.js +116 -71
  78. package/lib/module/components/Dialog/Dialog.js.map +1 -1
  79. package/lib/module/components/Dialog/StepDialog.js +107 -0
  80. package/lib/module/components/Dialog/StepDialog.js.map +1 -0
  81. package/lib/module/components/Dialog/StepDialog.stories.js +153 -0
  82. package/lib/module/components/Dialog/StepDialog.stories.js.map +1 -0
  83. package/lib/module/components/Dialog/StepDialog.test.js +117 -0
  84. package/lib/module/components/Dialog/StepDialog.test.js.map +1 -0
  85. package/lib/module/components/Dialog/index.js +1 -0
  86. package/lib/module/components/Dialog/index.js.map +1 -1
  87. package/lib/module/components/Icon/icons.generated.js +9 -1
  88. package/lib/module/components/Icon/icons.generated.js.map +1 -1
  89. package/lib/module/components/List/List.js +41 -0
  90. package/lib/module/components/List/List.js.map +1 -0
  91. package/lib/module/components/List/List.stories.js +211 -0
  92. package/lib/module/components/List/List.stories.js.map +1 -0
  93. package/lib/module/components/List/List.test.js +94 -0
  94. package/lib/module/components/List/List.test.js.map +1 -0
  95. package/lib/module/components/List/ListItem.js +206 -0
  96. package/lib/module/components/List/ListItem.js.map +1 -0
  97. package/lib/module/components/List/index.js +5 -0
  98. package/lib/module/components/List/index.js.map +1 -0
  99. package/lib/module/components/Menu/Menu.js +12 -4
  100. package/lib/module/components/Menu/Menu.js.map +1 -1
  101. package/lib/module/components/ProgressBar/ProgressBar.js +4 -3
  102. package/lib/module/components/ProgressBar/ProgressBar.js.map +1 -1
  103. package/lib/module/components/Sidebar/Sidebar.js +20 -3
  104. package/lib/module/components/Sidebar/Sidebar.js.map +1 -1
  105. package/lib/module/components/Sidebar/Sidebar.stories.js +16 -0
  106. package/lib/module/components/Sidebar/Sidebar.stories.js.map +1 -1
  107. package/lib/module/components/StatTile/StatTile.js +90 -0
  108. package/lib/module/components/StatTile/StatTile.js.map +1 -0
  109. package/lib/module/components/StatTile/StatTile.stories.js +96 -0
  110. package/lib/module/components/StatTile/StatTile.stories.js.map +1 -0
  111. package/lib/module/components/StatTile/StatTile.test.js +49 -0
  112. package/lib/module/components/StatTile/StatTile.test.js.map +1 -0
  113. package/lib/module/components/StatTile/index.js +4 -0
  114. package/lib/module/components/StatTile/index.js.map +1 -0
  115. package/lib/module/components/Table/Table.js +70 -7
  116. package/lib/module/components/Table/Table.js.map +1 -1
  117. package/lib/module/components/Table/Table.stories.js +117 -0
  118. package/lib/module/components/Table/Table.stories.js.map +1 -1
  119. package/lib/module/components/Table/Table.test.js +58 -0
  120. package/lib/module/components/Table/Table.test.js.map +1 -1
  121. package/lib/module/components/_internal/ModalHostContext.js +17 -0
  122. package/lib/module/components/_internal/ModalHostContext.js.map +1 -0
  123. package/lib/module/components/index.js +3 -0
  124. package/lib/module/components/index.js.map +1 -1
  125. package/lib/typescript/commonjs/components/Alert/Alert.d.ts +41 -0
  126. package/lib/typescript/commonjs/components/Alert/Alert.d.ts.map +1 -0
  127. package/lib/typescript/commonjs/components/Alert/index.d.ts +3 -0
  128. package/lib/typescript/commonjs/components/Alert/index.d.ts.map +1 -0
  129. package/lib/typescript/commonjs/components/CodeInput/CodeInput.d.ts +5 -1
  130. package/lib/typescript/commonjs/components/CodeInput/CodeInput.d.ts.map +1 -1
  131. package/lib/typescript/commonjs/components/Dialog/Dialog.d.ts +13 -1
  132. package/lib/typescript/commonjs/components/Dialog/Dialog.d.ts.map +1 -1
  133. package/lib/typescript/commonjs/components/Dialog/StepDialog.d.ts +63 -0
  134. package/lib/typescript/commonjs/components/Dialog/StepDialog.d.ts.map +1 -0
  135. package/lib/typescript/commonjs/components/Dialog/index.d.ts +1 -0
  136. package/lib/typescript/commonjs/components/Dialog/index.d.ts.map +1 -1
  137. package/lib/typescript/commonjs/components/Icon/icons.generated.d.ts +8 -0
  138. package/lib/typescript/commonjs/components/Icon/icons.generated.d.ts.map +1 -1
  139. package/lib/typescript/commonjs/components/List/List.d.ts +22 -0
  140. package/lib/typescript/commonjs/components/List/List.d.ts.map +1 -0
  141. package/lib/typescript/commonjs/components/List/ListItem.d.ts +61 -0
  142. package/lib/typescript/commonjs/components/List/ListItem.d.ts.map +1 -0
  143. package/lib/typescript/commonjs/components/List/index.d.ts +5 -0
  144. package/lib/typescript/commonjs/components/List/index.d.ts.map +1 -0
  145. package/lib/typescript/commonjs/components/Menu/Menu.d.ts +4 -2
  146. package/lib/typescript/commonjs/components/Menu/Menu.d.ts.map +1 -1
  147. package/lib/typescript/commonjs/components/ProgressBar/ProgressBar.d.ts +3 -1
  148. package/lib/typescript/commonjs/components/ProgressBar/ProgressBar.d.ts.map +1 -1
  149. package/lib/typescript/commonjs/components/Sidebar/Sidebar.d.ts.map +1 -1
  150. package/lib/typescript/commonjs/components/StatTile/StatTile.d.ts +31 -0
  151. package/lib/typescript/commonjs/components/StatTile/StatTile.d.ts.map +1 -0
  152. package/lib/typescript/commonjs/components/StatTile/index.d.ts +3 -0
  153. package/lib/typescript/commonjs/components/StatTile/index.d.ts.map +1 -0
  154. package/lib/typescript/commonjs/components/Table/Table.d.ts +25 -3
  155. package/lib/typescript/commonjs/components/Table/Table.d.ts.map +1 -1
  156. package/lib/typescript/commonjs/components/_internal/ModalHostContext.d.ts +11 -0
  157. package/lib/typescript/commonjs/components/_internal/ModalHostContext.d.ts.map +1 -0
  158. package/lib/typescript/commonjs/components/index.d.ts +3 -0
  159. package/lib/typescript/commonjs/components/index.d.ts.map +1 -1
  160. package/lib/typescript/module/components/Alert/Alert.d.ts +41 -0
  161. package/lib/typescript/module/components/Alert/Alert.d.ts.map +1 -0
  162. package/lib/typescript/module/components/Alert/index.d.ts +3 -0
  163. package/lib/typescript/module/components/Alert/index.d.ts.map +1 -0
  164. package/lib/typescript/module/components/CodeInput/CodeInput.d.ts +5 -1
  165. package/lib/typescript/module/components/CodeInput/CodeInput.d.ts.map +1 -1
  166. package/lib/typescript/module/components/Dialog/Dialog.d.ts +13 -1
  167. package/lib/typescript/module/components/Dialog/Dialog.d.ts.map +1 -1
  168. package/lib/typescript/module/components/Dialog/StepDialog.d.ts +63 -0
  169. package/lib/typescript/module/components/Dialog/StepDialog.d.ts.map +1 -0
  170. package/lib/typescript/module/components/Dialog/index.d.ts +1 -0
  171. package/lib/typescript/module/components/Dialog/index.d.ts.map +1 -1
  172. package/lib/typescript/module/components/Icon/icons.generated.d.ts +8 -0
  173. package/lib/typescript/module/components/Icon/icons.generated.d.ts.map +1 -1
  174. package/lib/typescript/module/components/List/List.d.ts +22 -0
  175. package/lib/typescript/module/components/List/List.d.ts.map +1 -0
  176. package/lib/typescript/module/components/List/ListItem.d.ts +61 -0
  177. package/lib/typescript/module/components/List/ListItem.d.ts.map +1 -0
  178. package/lib/typescript/module/components/List/index.d.ts +5 -0
  179. package/lib/typescript/module/components/List/index.d.ts.map +1 -0
  180. package/lib/typescript/module/components/Menu/Menu.d.ts +4 -2
  181. package/lib/typescript/module/components/Menu/Menu.d.ts.map +1 -1
  182. package/lib/typescript/module/components/ProgressBar/ProgressBar.d.ts +3 -1
  183. package/lib/typescript/module/components/ProgressBar/ProgressBar.d.ts.map +1 -1
  184. package/lib/typescript/module/components/Sidebar/Sidebar.d.ts.map +1 -1
  185. package/lib/typescript/module/components/StatTile/StatTile.d.ts +31 -0
  186. package/lib/typescript/module/components/StatTile/StatTile.d.ts.map +1 -0
  187. package/lib/typescript/module/components/StatTile/index.d.ts +3 -0
  188. package/lib/typescript/module/components/StatTile/index.d.ts.map +1 -0
  189. package/lib/typescript/module/components/Table/Table.d.ts +25 -3
  190. package/lib/typescript/module/components/Table/Table.d.ts.map +1 -1
  191. package/lib/typescript/module/components/_internal/ModalHostContext.d.ts +11 -0
  192. package/lib/typescript/module/components/_internal/ModalHostContext.d.ts.map +1 -0
  193. package/lib/typescript/module/components/index.d.ts +3 -0
  194. package/lib/typescript/module/components/index.d.ts.map +1 -1
  195. package/package.json +16 -14
  196. package/src/components/Alert/Alert.tsx +132 -0
  197. package/src/components/Alert/index.ts +2 -0
  198. package/src/components/CodeInput/CodeInput.tsx +46 -14
  199. package/src/components/Dialog/Dialog.tsx +92 -31
  200. package/src/components/Dialog/StepDialog.tsx +173 -0
  201. package/src/components/Dialog/index.ts +1 -0
  202. package/src/components/Icon/icons.generated.ts +9 -1
  203. package/src/components/List/List.tsx +45 -0
  204. package/src/components/List/ListItem.tsx +214 -0
  205. package/src/components/List/index.ts +4 -0
  206. package/src/components/Menu/Menu.tsx +15 -6
  207. package/src/components/ProgressBar/ProgressBar.tsx +6 -3
  208. package/src/components/Sidebar/Sidebar.tsx +18 -10
  209. package/src/components/StatTile/StatTile.tsx +98 -0
  210. package/src/components/StatTile/index.ts +2 -0
  211. package/src/components/Table/Table.tsx +93 -8
  212. package/src/components/_internal/ModalHostContext.tsx +15 -0
  213. package/src/components/index.ts +3 -0
@@ -1,4 +1,4 @@
1
- import { useEffect, useRef, useState, type ReactNode } from 'react';
1
+ import { useContext, useEffect, useRef, useState, type ReactNode } from 'react';
2
2
  import {
3
3
  Animated,
4
4
  Dimensions,
@@ -12,6 +12,7 @@ import {
12
12
  type StyleProp,
13
13
  type ViewStyle,
14
14
  } from 'react-native';
15
+ import { ModalHostContext } from '../_internal/ModalHostContext';
15
16
  import { computeMenuPlacement } from './placement';
16
17
 
17
18
  export type OriginVertical = 'top' | 'center' | 'bottom';
@@ -57,8 +58,10 @@ export interface MenuProps {
57
58
  insets?: EdgeInsets;
58
59
  /**
59
60
  * Offset between window coordinates (where the trigger is measured) and the
60
- * full-screen Modal the menu paints into. Defaults to the Android status bar
61
- * height (0 on iOS / web). Pass 0 when the host runs edge-to-edge / immersive.
61
+ * full-screen Modal the menu paints into. When omitted it defaults to the
62
+ * Android status bar height, or 0 when rendered inside a full-screen Modal host
63
+ * (e.g. `Dialog`) or on iOS / web. Pass an explicit value to override (e.g. 0
64
+ * when the host runs edge-to-edge / immersive).
62
65
  */
63
66
  statusBarHeight?: number;
64
67
  /** Controlled open state. Omit for uncontrolled. */
@@ -105,13 +108,18 @@ export function Menu({
105
108
  offset = 8,
106
109
  autoFit = true,
107
110
  insets = ZERO_INSETS,
108
- statusBarHeight = DEFAULT_STATUS_BAR_HEIGHT,
111
+ statusBarHeight,
109
112
  open: openProp,
110
113
  onOpenChange,
111
114
  screenPadding = 8,
112
115
  triggerStyle,
113
116
  }: MenuProps) {
114
117
  const triggerRef = useRef<View>(null);
118
+ // Inside a full-screen Modal host (e.g. Dialog) the trigger is already measured
119
+ // in full-screen coordinates, so no status-bar compensation is needed there.
120
+ const insideModalHost = useContext(ModalHostContext);
121
+ const effectiveStatusBarHeight =
122
+ statusBarHeight ?? (insideModalHost ? 0 : DEFAULT_STATUS_BAR_HEIGHT);
115
123
  const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
116
124
  const isControlled = openProp !== undefined;
117
125
  const open = isControlled ? openProp : uncontrolledOpen;
@@ -182,7 +190,7 @@ export function Menu({
182
190
  insets,
183
191
  screenWidth: screen.width,
184
192
  screenHeight: screen.height,
185
- statusBarHeight,
193
+ statusBarHeight: effectiveStatusBarHeight,
186
194
  autoFit,
187
195
  })
188
196
  : null;
@@ -231,7 +239,8 @@ export function Menu({
231
239
  transform: [{ scale: placed ? anim.interpolate({ inputRange: [0, 1], outputRange: [0.96, 1] }) : 1 }],
232
240
  }}
233
241
  >
234
- {content}
242
+ {/* The overlay is itself a full-screen Modal host for nested menus. */}
243
+ <ModalHostContext.Provider value={true}>{content}</ModalHostContext.Provider>
235
244
  </Animated.View>
236
245
  </Pressable>
237
246
  </Modal>
@@ -24,6 +24,8 @@ export interface ProgressBarProps {
24
24
  trackColor?: string;
25
25
  /** Track border color. Defaults to `border.default`. */
26
26
  borderColor?: string;
27
+ /** Draw the track border (and inner inset). Set false for a flat, thin bar. Defaults to true. */
28
+ bordered?: boolean;
27
29
  /** Animate width changes. Defaults to true. */
28
30
  animated?: boolean;
29
31
  /** Override the container style. */
@@ -51,6 +53,7 @@ export function ProgressBar({
51
53
  color,
52
54
  trackColor,
53
55
  borderColor,
56
+ bordered = true,
54
57
  animated = true,
55
58
  style,
56
59
  accessibilityLabel,
@@ -79,7 +82,7 @@ export function ProgressBar({
79
82
  extrapolate: 'clamp',
80
83
  });
81
84
 
82
- const inset = 2.5;
85
+ const inset = bordered ? 2.5 : 0;
83
86
 
84
87
  return (
85
88
  <View
@@ -92,8 +95,8 @@ export function ProgressBar({
92
95
  style={{
93
96
  height,
94
97
  borderRadius: height / 2,
95
- borderWidth: 1.5,
96
- borderColor: borderColor ?? theme.colors.border.default,
98
+ borderWidth: bordered ? 1.5 : 0,
99
+ borderColor: bordered ? borderColor ?? theme.colors.border.default : 'transparent',
97
100
  backgroundColor: trackColor ?? theme.colors.background.subtle,
98
101
  padding: inset,
99
102
  justifyContent: 'center',
@@ -234,6 +234,15 @@ export function Sidebar({
234
234
  extrapolate: 'clamp',
235
235
  });
236
236
 
237
+ // The logo glides to center the same way (measured so any custom logo works),
238
+ // at a constant size — no discrete size/alignment swap, so no flicker.
239
+ const [logoWidth, setLogoWidth] = useState(0);
240
+ const logoTranslateX = width.interpolate({
241
+ inputRange: [collapsedWidth, expandedWidth],
242
+ outputRange: [Math.max(0, (collapsedWidth - 2 * PADDING - logoWidth) / 2), 0],
243
+ extrapolate: 'clamp',
244
+ });
245
+
237
246
  const selectItem = (item: SidebarItem) => {
238
247
  if (!activeControlled) setActiveInternal(item.key);
239
248
  item.onPress?.();
@@ -246,7 +255,7 @@ export function Sidebar({
246
255
  onCollapsedChange?.(next);
247
256
  };
248
257
 
249
- const logoNode = logo ?? <BaconLogo height={visualCollapsed ? 36 : 40} />;
258
+ const logoNode = logo ?? <BaconLogo height={40} />;
250
259
  const toggle = showToggle ? (
251
260
  <Animated.View style={{ transform: [{ translateX: toggleTranslateX }] }}>
252
261
  <Animated.View style={{ transform: [{ rotate: arrowRotate }] }}>
@@ -276,15 +285,14 @@ export function Sidebar({
276
285
  style,
277
286
  ]}
278
287
  >
279
- {/* Logo */}
280
- <View
281
- style={{
282
- minHeight: 56,
283
- alignItems: visualCollapsed ? 'center' : 'flex-start',
284
- justifyContent: 'center',
285
- }}
286
- >
287
- {logoNode}
288
+ {/* Logo — constant size, glides to center on collapse (no flicker) */}
289
+ <View style={{ minHeight: 56, alignItems: 'flex-start', justifyContent: 'center' }}>
290
+ <Animated.View
291
+ onLayout={(e) => setLogoWidth(e.nativeEvent.layout.width)}
292
+ style={{ transform: [{ translateX: logoTranslateX }] }}
293
+ >
294
+ {logoNode}
295
+ </Animated.View>
288
296
  </View>
289
297
 
290
298
  {/* Tabs (scrolls when overflowing; footer stays pinned below) */}
@@ -0,0 +1,98 @@
1
+ import { type ReactNode } from 'react';
2
+ import { Pressable, View, type StyleProp, type ViewStyle } from 'react-native';
3
+ import { useTheme } from '../../theme';
4
+ import { Icon } from '../Icon/Icon';
5
+ import type { IconName } from '../Icon/icons.generated';
6
+ import { Text } from '../Text/Text';
7
+
8
+ export interface StatTileProps {
9
+ /** Small label above the value. */
10
+ label: string;
11
+ /** The value — a string/number, or a node for custom formatting/color. */
12
+ value: ReactNode;
13
+ /** Optional leading icon. */
14
+ icon?: IconName;
15
+ /** Fixed width. Omit to fill the parent (works in a flex/grid row of tiles). */
16
+ width?: number;
17
+ /** Make the tile pressable. */
18
+ onPress?: () => void;
19
+ /** Tile background. Defaults to `background.subtle`. */
20
+ backgroundColor?: string;
21
+ /** Label color. Defaults to `secondary`. */
22
+ labelColor?: string;
23
+ /** Value color (when `value` is text). Defaults to `primary`. */
24
+ valueColor?: string;
25
+ /** Override the container style. */
26
+ style?: StyleProp<ViewStyle>;
27
+ accessibilityLabel?: string;
28
+ }
29
+
30
+ /**
31
+ * StatTile — a compact KPI card: a small label over a prominent value (Figma
32
+ * "info card"). Fills its parent by default (so a row/grid of tiles sizes evenly)
33
+ * or takes a fixed `width`. `value` accepts a node for custom color/formatting.
34
+ */
35
+ export function StatTile({
36
+ label,
37
+ value,
38
+ icon,
39
+ width,
40
+ onPress,
41
+ backgroundColor,
42
+ labelColor,
43
+ valueColor,
44
+ style,
45
+ accessibilityLabel,
46
+ }: StatTileProps) {
47
+ const theme = useTheme();
48
+
49
+ const content = (
50
+ <>
51
+ {icon ? (
52
+ <View style={{ width: 24, height: 24, flexShrink: 0 }}>
53
+ <Icon name={icon} size={24} color="secondary" />
54
+ </View>
55
+ ) : null}
56
+ <View style={{ flex: 1, minWidth: 0, gap: 4 }}>
57
+ <Text variant="bodySmall" color={labelColor ?? 'secondary'} numberOfLines={1}>
58
+ {label}
59
+ </Text>
60
+ {typeof value === 'string' || typeof value === 'number' ? (
61
+ <Text variant="bodyLarge" weight="semibold" color={valueColor ?? 'primary'} numberOfLines={1}>
62
+ {String(value)}
63
+ </Text>
64
+ ) : (
65
+ value
66
+ )}
67
+ </View>
68
+ </>
69
+ );
70
+
71
+ const containerStyle: ViewStyle = {
72
+ flexDirection: 'row',
73
+ alignItems: 'center',
74
+ gap: 12,
75
+ padding: 12,
76
+ borderRadius: theme.radii.lg,
77
+ backgroundColor: backgroundColor ?? theme.colors.background.subtle,
78
+ ...(width != null ? { width } : { alignSelf: 'stretch' }),
79
+ };
80
+
81
+ if (!onPress) {
82
+ return (
83
+ <View accessibilityLabel={accessibilityLabel} style={[containerStyle, style]}>
84
+ {content}
85
+ </View>
86
+ );
87
+ }
88
+ return (
89
+ <Pressable
90
+ onPress={onPress}
91
+ accessibilityRole="button"
92
+ accessibilityLabel={accessibilityLabel ?? label}
93
+ style={({ pressed }) => [containerStyle, { opacity: pressed ? 0.96 : 1 }, style]}
94
+ >
95
+ {content}
96
+ </Pressable>
97
+ );
98
+ }
@@ -0,0 +1,2 @@
1
+ export { StatTile } from './StatTile';
2
+ export type { StatTileProps } from './StatTile';
@@ -11,13 +11,30 @@ import {
11
11
  type ViewStyle,
12
12
  } from 'react-native';
13
13
  import { useTheme } from '../../theme';
14
+ import type { DropdownEntry } from '../Dropdown/Dropdown';
14
15
  import { Checkbox } from '../Checkbox/Checkbox';
15
16
  import { Icon } from '../Icon/Icon';
17
+ import type { IconName } from '../Icon/icons.generated';
18
+ import { MenuButton } from '../Menu/MenuButton';
16
19
  import { Spinner } from '../Spinner/Spinner';
17
20
  import { Text } from '../Text/Text';
18
21
  import { ScrollArea } from '../_internal/ScrollArea';
19
22
 
20
23
  export type ColumnAlign = 'left' | 'center' | 'right';
24
+
25
+ /** A structured row action (inline icon, or a row in the overflow "More" menu). */
26
+ export interface TableAction {
27
+ key: string;
28
+ label: string;
29
+ icon: IconName;
30
+ onPress?: () => void;
31
+ /** Render in the error color (red icon inline, red row in the menu). */
32
+ destructive?: boolean;
33
+ /** Dim and block the action. */
34
+ disabled?: boolean;
35
+ /** Icon color override (ignored when `destructive`). */
36
+ color?: string;
37
+ }
21
38
  export type SortDirection = 'asc' | 'desc';
22
39
  export interface TableSort {
23
40
  key: string;
@@ -63,9 +80,17 @@ export interface TableProps<T> {
63
80
  defaultExpandedKeys?: string[];
64
81
  onExpandedChange?: (keys: string[]) => void;
65
82
 
66
- /** Trailing per-row actions (e.g. edit/delete) — mutually exclusive with `expandable`. */
83
+ /** Trailing per-row actions, free-form (e.g. custom buttons) — mutually exclusive with `expandable`. */
67
84
  renderActions?: (row: T, index: number) => ReactNode;
68
- /** Width reserved for the actions column. Defaults to 96. */
85
+ /** Structured per-row actions: the first `maxInlineActions` render as inline icons, the rest collapse into a "More" menu. */
86
+ actions?: (row: T, index: number) => TableAction[];
87
+ /** How many actions render inline before overflowing into the More menu. Defaults to 2. */
88
+ maxInlineActions?: number;
89
+ /** Icon for the overflow menu trigger. Defaults to `more`. */
90
+ moreIcon?: IconName;
91
+ /** Width of the overflow More menu. */
92
+ actionsMenuWidth?: number;
93
+ /** Width reserved for the actions column. Defaults to fit the actions. */
69
94
  actionsWidth?: number;
70
95
 
71
96
  /** Row selection via checkboxes. */
@@ -135,7 +160,11 @@ export function Table<T>({
135
160
  defaultExpandedKeys,
136
161
  onExpandedChange,
137
162
  renderActions,
138
- actionsWidth = 96,
163
+ actions,
164
+ maxInlineActions = 2,
165
+ moreIcon = 'more',
166
+ actionsMenuWidth,
167
+ actionsWidth,
139
168
  selectable = false,
140
169
  selectedKeys,
141
170
  defaultSelectedKeys,
@@ -159,8 +188,11 @@ export function Table<T>({
159
188
  const isMobile = screenWidth < mobileBreakpoint;
160
189
 
161
190
  // Expandable and actions are mutually exclusive; expandable wins if both set.
162
- const hasActions = !expandable && !!renderActions;
163
- const trailingWidth = expandable ? CHEVRON_WIDTH : hasActions ? actionsWidth : 0;
191
+ const hasActions = !expandable && (!!renderActions || !!actions);
192
+ const ACTION_SLOT = 44; // sm icon-button footprint (40 + gap)
193
+ const actionsColWidth =
194
+ actionsWidth ?? (renderActions ? 96 : (maxInlineActions + 1) * ACTION_SLOT);
195
+ const trailingWidth = expandable ? CHEVRON_WIDTH : hasActions ? actionsColWidth : 0;
164
196
 
165
197
  // ---- sort ----
166
198
  const sortControlled = sort !== undefined;
@@ -245,6 +277,57 @@ export function Table<T>({
245
277
  return content as ReactNode;
246
278
  };
247
279
 
280
+ // Actions cell: free-form `renderActions`, or structured `actions` with the
281
+ // first N inline and the rest in a "More" menu (all in the menu on mobile).
282
+ const renderActionsCell = (row: T, index: number): ReactNode => {
283
+ if (renderActions) return renderActions(row, index);
284
+ if (!actions) return null;
285
+ const list = actions(row, index);
286
+ const inlineCount = isMobile ? 0 : maxInlineActions;
287
+ const inline = list.slice(0, inlineCount);
288
+ const overflow = list.slice(inlineCount);
289
+ return (
290
+ <>
291
+ {inline.map((a) => (
292
+ <Pressable
293
+ key={a.key}
294
+ onPress={a.disabled ? undefined : a.onPress}
295
+ disabled={a.disabled}
296
+ hitSlop={8}
297
+ accessibilityRole="button"
298
+ accessibilityLabel={a.label}
299
+ style={{ width: 40, height: 40, alignItems: 'center', justifyContent: 'center', opacity: a.disabled ? 0.5 : 1 }}
300
+ >
301
+ <Icon
302
+ name={a.icon}
303
+ size={24}
304
+ color={a.destructive ? theme.colors.feedback.error : a.color ?? theme.colors.text.secondary}
305
+ />
306
+ </Pressable>
307
+ ))}
308
+ {overflow.length > 0 ? (
309
+ <MenuButton
310
+ icon={moreIcon}
311
+ accessibilityLabel="More actions"
312
+ variant="ghost"
313
+ size="sm"
314
+ menuWidth={actionsMenuWidth}
315
+ options={overflow.map(
316
+ (a): DropdownEntry => ({
317
+ key: a.key,
318
+ label: a.label,
319
+ disabled: a.disabled,
320
+ destructive: a.destructive,
321
+ leading: { kind: 'icon', name: a.icon, ...(a.destructive || !a.color ? {} : { color: a.color }) },
322
+ onPress: a.onPress,
323
+ }),
324
+ )}
325
+ />
326
+ ) : null}
327
+ </>
328
+ );
329
+ };
330
+
248
331
  // ---------- Desktop ----------
249
332
  const header = (
250
333
  <View
@@ -347,8 +430,8 @@ export function Table<T>({
347
430
  <Icon name="chevron-down" size={24} color="secondary" />
348
431
  </View>
349
432
  ) : hasActions ? (
350
- <View style={{ width: actionsWidth, flexShrink: 0, flexDirection: 'row', justifyContent: 'flex-end', gap: 4 }}>
351
- {renderActions!(row, index)}
433
+ <View style={{ width: actionsColWidth, flexShrink: 0, flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-end', gap: 4 }}>
434
+ {renderActionsCell(row, index)}
352
435
  </View>
353
436
  ) : null}
354
437
  </Pressable>
@@ -418,7 +501,9 @@ export function Table<T>({
418
501
  ) : null}
419
502
  </View>
420
503
  {hasActions ? (
421
- <View style={{ flexDirection: 'row', justifyContent: 'flex-end', gap: 4 }}>{renderActions!(row, index)}</View>
504
+ <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-end', gap: 4 }}>
505
+ {renderActionsCell(row, index)}
506
+ </View>
422
507
  ) : null}
423
508
  </Pressable>
424
509
  {expandable && isOpen && renderExpanded ? (
@@ -0,0 +1,15 @@
1
+ import { createContext, useContext } from 'react';
2
+
3
+ /**
4
+ * True when the subtree is rendered inside a full-screen (statusBarTranslucent)
5
+ * Modal host — e.g. `Dialog`, or a `Menu` overlay. Anchored menus use this to
6
+ * skip the status-bar coordinate compensation: inside such a host, a trigger
7
+ * measured with `measureInWindow` is already in full-screen coordinates that
8
+ * match the menu's own Modal, so adding the status bar height would push the
9
+ * menu down (a visible gap). Outside a host, the compensation is still applied.
10
+ */
11
+ export const ModalHostContext = createContext(false);
12
+
13
+ export function useInsideModalHost(): boolean {
14
+ return useContext(ModalHostContext);
15
+ }
@@ -1,3 +1,4 @@
1
+ export * from './Alert';
1
2
  export * from './Avatar';
2
3
  export * from './Badge';
3
4
  export * from './brand';
@@ -15,6 +16,7 @@ export * from './IconButton';
15
16
  export * from './Input';
16
17
  export * from './Keypad';
17
18
  export * from './LinkButton';
19
+ export * from './List';
18
20
  export * from './Menu';
19
21
  export * from './ProductCard';
20
22
  export * from './ProgressBar';
@@ -23,6 +25,7 @@ export * from './Radio';
23
25
  export * from './Select';
24
26
  export * from './Sidebar';
25
27
  export * from './Spinner';
28
+ export * from './StatTile';
26
29
  export * from './Surface';
27
30
  export * from './Switch';
28
31
  export * from './Table';