@bacon-pos/ui 0.11.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 (161) hide show
  1. package/lib/commonjs/components/CodeInput/CodeInput.js +28 -9
  2. package/lib/commonjs/components/CodeInput/CodeInput.js.map +1 -1
  3. package/lib/commonjs/components/CodeInput/CodeInput.stories.js +28 -1
  4. package/lib/commonjs/components/CodeInput/CodeInput.stories.js.map +1 -1
  5. package/lib/commonjs/components/CodeInput/CodeInput.test.js +16 -0
  6. package/lib/commonjs/components/CodeInput/CodeInput.test.js.map +1 -1
  7. package/lib/commonjs/components/Dialog/Dialog.js +116 -71
  8. package/lib/commonjs/components/Dialog/Dialog.js.map +1 -1
  9. package/lib/commonjs/components/Dialog/StepDialog.js +111 -0
  10. package/lib/commonjs/components/Dialog/StepDialog.js.map +1 -0
  11. package/lib/commonjs/components/Dialog/StepDialog.stories.js +157 -0
  12. package/lib/commonjs/components/Dialog/StepDialog.stories.js.map +1 -0
  13. package/lib/commonjs/components/Dialog/StepDialog.test.js +118 -0
  14. package/lib/commonjs/components/Dialog/StepDialog.test.js.map +1 -0
  15. package/lib/commonjs/components/Dialog/index.js +7 -0
  16. package/lib/commonjs/components/Dialog/index.js.map +1 -1
  17. package/lib/commonjs/components/Icon/icons.generated.js +9 -1
  18. package/lib/commonjs/components/Icon/icons.generated.js.map +1 -1
  19. package/lib/commonjs/components/List/List.js +45 -0
  20. package/lib/commonjs/components/List/List.js.map +1 -0
  21. package/lib/commonjs/components/List/List.stories.js +215 -0
  22. package/lib/commonjs/components/List/List.stories.js.map +1 -0
  23. package/lib/commonjs/components/List/List.test.js +94 -0
  24. package/lib/commonjs/components/List/List.test.js.map +1 -0
  25. package/lib/commonjs/components/List/ListItem.js +210 -0
  26. package/lib/commonjs/components/List/ListItem.js.map +1 -0
  27. package/lib/commonjs/components/List/index.js +20 -0
  28. package/lib/commonjs/components/List/index.js.map +1 -0
  29. package/lib/commonjs/components/Menu/Menu.js +11 -3
  30. package/lib/commonjs/components/Menu/Menu.js.map +1 -1
  31. package/lib/commonjs/components/ProgressBar/ProgressBar.js +4 -3
  32. package/lib/commonjs/components/ProgressBar/ProgressBar.js.map +1 -1
  33. package/lib/commonjs/components/Sidebar/Sidebar.js +20 -3
  34. package/lib/commonjs/components/Sidebar/Sidebar.js.map +1 -1
  35. package/lib/commonjs/components/Sidebar/Sidebar.stories.js +17 -1
  36. package/lib/commonjs/components/Sidebar/Sidebar.stories.js.map +1 -1
  37. package/lib/commonjs/components/Table/Table.js +69 -6
  38. package/lib/commonjs/components/Table/Table.js.map +1 -1
  39. package/lib/commonjs/components/Table/Table.stories.js +118 -1
  40. package/lib/commonjs/components/Table/Table.stories.js.map +1 -1
  41. package/lib/commonjs/components/Table/Table.test.js +58 -0
  42. package/lib/commonjs/components/Table/Table.test.js.map +1 -1
  43. package/lib/commonjs/components/_internal/ModalHostContext.js +21 -0
  44. package/lib/commonjs/components/_internal/ModalHostContext.js.map +1 -0
  45. package/lib/commonjs/components/index.js +26 -15
  46. package/lib/commonjs/components/index.js.map +1 -1
  47. package/lib/module/components/CodeInput/CodeInput.js +28 -9
  48. package/lib/module/components/CodeInput/CodeInput.js.map +1 -1
  49. package/lib/module/components/CodeInput/CodeInput.stories.js +27 -0
  50. package/lib/module/components/CodeInput/CodeInput.stories.js.map +1 -1
  51. package/lib/module/components/CodeInput/CodeInput.test.js +16 -0
  52. package/lib/module/components/CodeInput/CodeInput.test.js.map +1 -1
  53. package/lib/module/components/Dialog/Dialog.js +116 -71
  54. package/lib/module/components/Dialog/Dialog.js.map +1 -1
  55. package/lib/module/components/Dialog/StepDialog.js +107 -0
  56. package/lib/module/components/Dialog/StepDialog.js.map +1 -0
  57. package/lib/module/components/Dialog/StepDialog.stories.js +153 -0
  58. package/lib/module/components/Dialog/StepDialog.stories.js.map +1 -0
  59. package/lib/module/components/Dialog/StepDialog.test.js +117 -0
  60. package/lib/module/components/Dialog/StepDialog.test.js.map +1 -0
  61. package/lib/module/components/Dialog/index.js +1 -0
  62. package/lib/module/components/Dialog/index.js.map +1 -1
  63. package/lib/module/components/Icon/icons.generated.js +9 -1
  64. package/lib/module/components/Icon/icons.generated.js.map +1 -1
  65. package/lib/module/components/List/List.js +41 -0
  66. package/lib/module/components/List/List.js.map +1 -0
  67. package/lib/module/components/List/List.stories.js +211 -0
  68. package/lib/module/components/List/List.stories.js.map +1 -0
  69. package/lib/module/components/List/List.test.js +94 -0
  70. package/lib/module/components/List/List.test.js.map +1 -0
  71. package/lib/module/components/List/ListItem.js +206 -0
  72. package/lib/module/components/List/ListItem.js.map +1 -0
  73. package/lib/module/components/List/index.js +5 -0
  74. package/lib/module/components/List/index.js.map +1 -0
  75. package/lib/module/components/Menu/Menu.js +12 -4
  76. package/lib/module/components/Menu/Menu.js.map +1 -1
  77. package/lib/module/components/ProgressBar/ProgressBar.js +4 -3
  78. package/lib/module/components/ProgressBar/ProgressBar.js.map +1 -1
  79. package/lib/module/components/Sidebar/Sidebar.js +20 -3
  80. package/lib/module/components/Sidebar/Sidebar.js.map +1 -1
  81. package/lib/module/components/Sidebar/Sidebar.stories.js +16 -0
  82. package/lib/module/components/Sidebar/Sidebar.stories.js.map +1 -1
  83. package/lib/module/components/Table/Table.js +70 -7
  84. package/lib/module/components/Table/Table.js.map +1 -1
  85. package/lib/module/components/Table/Table.stories.js +117 -0
  86. package/lib/module/components/Table/Table.stories.js.map +1 -1
  87. package/lib/module/components/Table/Table.test.js +58 -0
  88. package/lib/module/components/Table/Table.test.js.map +1 -1
  89. package/lib/module/components/_internal/ModalHostContext.js +17 -0
  90. package/lib/module/components/_internal/ModalHostContext.js.map +1 -0
  91. package/lib/module/components/index.js +1 -0
  92. package/lib/module/components/index.js.map +1 -1
  93. package/lib/typescript/commonjs/components/CodeInput/CodeInput.d.ts +5 -1
  94. package/lib/typescript/commonjs/components/CodeInput/CodeInput.d.ts.map +1 -1
  95. package/lib/typescript/commonjs/components/Dialog/Dialog.d.ts +13 -1
  96. package/lib/typescript/commonjs/components/Dialog/Dialog.d.ts.map +1 -1
  97. package/lib/typescript/commonjs/components/Dialog/StepDialog.d.ts +63 -0
  98. package/lib/typescript/commonjs/components/Dialog/StepDialog.d.ts.map +1 -0
  99. package/lib/typescript/commonjs/components/Dialog/index.d.ts +1 -0
  100. package/lib/typescript/commonjs/components/Dialog/index.d.ts.map +1 -1
  101. package/lib/typescript/commonjs/components/Icon/icons.generated.d.ts +8 -0
  102. package/lib/typescript/commonjs/components/Icon/icons.generated.d.ts.map +1 -1
  103. package/lib/typescript/commonjs/components/List/List.d.ts +22 -0
  104. package/lib/typescript/commonjs/components/List/List.d.ts.map +1 -0
  105. package/lib/typescript/commonjs/components/List/ListItem.d.ts +61 -0
  106. package/lib/typescript/commonjs/components/List/ListItem.d.ts.map +1 -0
  107. package/lib/typescript/commonjs/components/List/index.d.ts +5 -0
  108. package/lib/typescript/commonjs/components/List/index.d.ts.map +1 -0
  109. package/lib/typescript/commonjs/components/Menu/Menu.d.ts +4 -2
  110. package/lib/typescript/commonjs/components/Menu/Menu.d.ts.map +1 -1
  111. package/lib/typescript/commonjs/components/ProgressBar/ProgressBar.d.ts +3 -1
  112. package/lib/typescript/commonjs/components/ProgressBar/ProgressBar.d.ts.map +1 -1
  113. package/lib/typescript/commonjs/components/Sidebar/Sidebar.d.ts.map +1 -1
  114. package/lib/typescript/commonjs/components/Table/Table.d.ts +25 -3
  115. package/lib/typescript/commonjs/components/Table/Table.d.ts.map +1 -1
  116. package/lib/typescript/commonjs/components/_internal/ModalHostContext.d.ts +11 -0
  117. package/lib/typescript/commonjs/components/_internal/ModalHostContext.d.ts.map +1 -0
  118. package/lib/typescript/commonjs/components/index.d.ts +1 -0
  119. package/lib/typescript/commonjs/components/index.d.ts.map +1 -1
  120. package/lib/typescript/module/components/CodeInput/CodeInput.d.ts +5 -1
  121. package/lib/typescript/module/components/CodeInput/CodeInput.d.ts.map +1 -1
  122. package/lib/typescript/module/components/Dialog/Dialog.d.ts +13 -1
  123. package/lib/typescript/module/components/Dialog/Dialog.d.ts.map +1 -1
  124. package/lib/typescript/module/components/Dialog/StepDialog.d.ts +63 -0
  125. package/lib/typescript/module/components/Dialog/StepDialog.d.ts.map +1 -0
  126. package/lib/typescript/module/components/Dialog/index.d.ts +1 -0
  127. package/lib/typescript/module/components/Dialog/index.d.ts.map +1 -1
  128. package/lib/typescript/module/components/Icon/icons.generated.d.ts +8 -0
  129. package/lib/typescript/module/components/Icon/icons.generated.d.ts.map +1 -1
  130. package/lib/typescript/module/components/List/List.d.ts +22 -0
  131. package/lib/typescript/module/components/List/List.d.ts.map +1 -0
  132. package/lib/typescript/module/components/List/ListItem.d.ts +61 -0
  133. package/lib/typescript/module/components/List/ListItem.d.ts.map +1 -0
  134. package/lib/typescript/module/components/List/index.d.ts +5 -0
  135. package/lib/typescript/module/components/List/index.d.ts.map +1 -0
  136. package/lib/typescript/module/components/Menu/Menu.d.ts +4 -2
  137. package/lib/typescript/module/components/Menu/Menu.d.ts.map +1 -1
  138. package/lib/typescript/module/components/ProgressBar/ProgressBar.d.ts +3 -1
  139. package/lib/typescript/module/components/ProgressBar/ProgressBar.d.ts.map +1 -1
  140. package/lib/typescript/module/components/Sidebar/Sidebar.d.ts.map +1 -1
  141. package/lib/typescript/module/components/Table/Table.d.ts +25 -3
  142. package/lib/typescript/module/components/Table/Table.d.ts.map +1 -1
  143. package/lib/typescript/module/components/_internal/ModalHostContext.d.ts +11 -0
  144. package/lib/typescript/module/components/_internal/ModalHostContext.d.ts.map +1 -0
  145. package/lib/typescript/module/components/index.d.ts +1 -0
  146. package/lib/typescript/module/components/index.d.ts.map +1 -1
  147. package/package.json +16 -14
  148. package/src/components/CodeInput/CodeInput.tsx +46 -14
  149. package/src/components/Dialog/Dialog.tsx +92 -31
  150. package/src/components/Dialog/StepDialog.tsx +173 -0
  151. package/src/components/Dialog/index.ts +1 -0
  152. package/src/components/Icon/icons.generated.ts +9 -1
  153. package/src/components/List/List.tsx +45 -0
  154. package/src/components/List/ListItem.tsx +214 -0
  155. package/src/components/List/index.ts +4 -0
  156. package/src/components/Menu/Menu.tsx +15 -6
  157. package/src/components/ProgressBar/ProgressBar.tsx +6 -3
  158. package/src/components/Sidebar/Sidebar.tsx +18 -10
  159. package/src/components/Table/Table.tsx +93 -8
  160. package/src/components/_internal/ModalHostContext.tsx +15 -0
  161. package/src/components/index.ts +1 -0
@@ -30,6 +30,10 @@ export interface CodeInputProps {
30
30
  disabled?: boolean;
31
31
  /** Focus on mount. */
32
32
  autoFocus?: boolean;
33
+ /** Mark the field invalid — turns the cells red. Pass a string to show it as a message below. */
34
+ error?: boolean | string;
35
+ /** Helper/message text below the cells. Replaced by the error message when `error` is a string. */
36
+ helperText?: string;
33
37
  /** Show an eye toggle to reveal/hide the entered digits. */
34
38
  revealable?: boolean;
35
39
  /** Controlled reveal state. Omit for uncontrolled (toggled by the eye). */
@@ -52,21 +56,28 @@ function Cell({
52
56
  filled,
53
57
  revealed,
54
58
  disabled,
59
+ error,
55
60
  theme,
56
61
  }: {
57
62
  char: string;
58
63
  filled: boolean;
59
64
  revealed: boolean;
60
65
  disabled: boolean;
66
+ error: boolean;
61
67
  theme: Theme;
62
68
  }) {
69
+ const errorColor = theme.colors.feedback.error;
63
70
  return (
64
71
  <View
65
72
  testID="code-input-cell"
66
73
  style={{ width: 20, height: 20, alignItems: 'center', justifyContent: 'center' }}
67
74
  >
68
75
  {revealed && filled ? (
69
- <Text variant="bodyMedium" weight="medium" color={disabled ? 'tertiary' : 'primary'}>
76
+ <Text
77
+ variant="bodyMedium"
78
+ weight="medium"
79
+ color={disabled ? 'tertiary' : error ? errorColor : 'primary'}
80
+ >
70
81
  {char}
71
82
  </Text>
72
83
  ) : (
@@ -76,8 +87,16 @@ function Cell({
76
87
  height: 20,
77
88
  borderRadius: 10,
78
89
  borderWidth: 1.5,
79
- backgroundColor: filled ? theme.colors.text.primary : theme.colors.background.default,
80
- borderColor: filled ? theme.colors.text.primary : theme.colors.border.strong,
90
+ backgroundColor: filled
91
+ ? error
92
+ ? errorColor
93
+ : theme.colors.text.primary
94
+ : theme.colors.background.default,
95
+ borderColor: error
96
+ ? errorColor
97
+ : filled
98
+ ? theme.colors.text.primary
99
+ : theme.colors.border.strong,
81
100
  opacity: disabled ? 0.5 : 1,
82
101
  }}
83
102
  />
@@ -100,6 +119,8 @@ export function CodeInput({
100
119
  onComplete,
101
120
  disabled = false,
102
121
  autoFocus = false,
122
+ error,
123
+ helperText,
103
124
  revealable = false,
104
125
  revealed: revealedProp,
105
126
  onRevealChange,
@@ -114,6 +135,8 @@ export function CodeInput({
114
135
  const [internalRevealed, setInternalRevealed] = useState(false);
115
136
  const revealed = revealedProp ?? internalRevealed;
116
137
  const filled = Math.min(value.length, length);
138
+ const hasError = Boolean(error);
139
+ const message = typeof error === 'string' ? error : helperText;
117
140
 
118
141
  const handleChange = (text: string) => {
119
142
  const digits = text.replace(/[^0-9]/g, '').slice(0, length);
@@ -141,6 +164,7 @@ export function CodeInput({
141
164
  filled={i < filled}
142
165
  revealed={revealed}
143
166
  disabled={disabled}
167
+ error={hasError}
144
168
  theme={theme}
145
169
  />
146
170
  ))}
@@ -180,21 +204,29 @@ export function CodeInput({
180
204
  const eyeFirst = revealPosition === 'top' || revealPosition === 'left';
181
205
 
182
206
  return (
183
- <View
184
- style={[
185
- {
207
+ <View style={[{ alignSelf: 'flex-start' }, style]}>
208
+ <View
209
+ style={{
186
210
  flexDirection: vertical ? 'column' : 'row',
187
211
  alignItems: 'center',
188
212
  gap: 12,
189
- alignSelf: 'flex-start',
190
213
  paddingVertical: 8,
191
- },
192
- style,
193
- ]}
194
- >
195
- {eyeFirst ? eyeToggle : null}
196
- {cells}
197
- {eyeFirst ? null : eyeToggle}
214
+ }}
215
+ >
216
+ {eyeFirst ? eyeToggle : null}
217
+ {cells}
218
+ {eyeFirst ? null : eyeToggle}
219
+ </View>
220
+
221
+ {message ? (
222
+ <Text
223
+ variant="bodySmall"
224
+ color={hasError ? theme.colors.feedback.error : 'tertiary'}
225
+ style={{ marginTop: 4, marginLeft: 8 }}
226
+ >
227
+ {message}
228
+ </Text>
229
+ ) : null}
198
230
  </View>
199
231
  );
200
232
  }
@@ -14,7 +14,9 @@ import {
14
14
  import { useTheme } from '../../theme';
15
15
  import { Button, type ButtonVariant } from '../Button/Button';
16
16
  import { Icon } from '../Icon/Icon';
17
+ import { IconButton } from '../IconButton/IconButton';
17
18
  import { Text } from '../Text/Text';
19
+ import { ModalHostContext } from '../_internal/ModalHostContext';
18
20
  import { ScrollArea } from '../_internal/ScrollArea';
19
21
 
20
22
  export interface DialogAction {
@@ -44,6 +46,18 @@ export interface DialogProps {
44
46
  title: string;
45
47
  /** Optional subtitle. */
46
48
  subtitle?: string;
49
+ /**
50
+ * Show a round back button to the left of the title and wire its press here.
51
+ * Used for multi-step flows (see `StepDialog`). Omit for a plain dialog.
52
+ */
53
+ onBack?: () => void;
54
+ /** Accessibility label for the back button. Defaults to "Back". */
55
+ backAccessibilityLabel?: string;
56
+ /**
57
+ * Content pinned in the footer, above the action buttons (e.g. a step
58
+ * progress bar). Centered. Shows the footer region even without actions.
59
+ */
60
+ footerAccessory?: ReactNode;
47
61
  /** Any content — from text to a full form. */
48
62
  children?: ReactNode;
49
63
  /** Primary/secondary action buttons (right side). Keep total buttons ≤ 3. */
@@ -99,6 +113,9 @@ export function Dialog({
99
113
  onClose,
100
114
  title,
101
115
  subtitle,
116
+ onBack,
117
+ backAccessibilityLabel = 'Back',
118
+ footerAccessory,
102
119
  children,
103
120
  actions = [],
104
121
  cancel,
@@ -140,54 +157,94 @@ export function Dialog({
140
157
 
141
158
  const header = (
142
159
  <View style={{ paddingHorizontal: padding, paddingTop: padding, paddingBottom: contentSpacing }}>
143
- <View style={{ paddingRight: showClose ? 40 : 0, gap: 4 }}>
144
- <Text variant="h3" weight="semibold" color="primary">
145
- {title}
146
- </Text>
147
- {subtitle ? (
148
- <Text variant="bodyLarge" color="secondary">
149
- {subtitle}
150
- </Text>
160
+ <View
161
+ style={{
162
+ flexDirection: 'row',
163
+ alignItems: 'center',
164
+ gap: 20,
165
+ paddingRight: showClose ? 40 : 0,
166
+ }}
167
+ >
168
+ {onBack ? (
169
+ <IconButton
170
+ icon="arrow-left"
171
+ accessibilityLabel={backAccessibilityLabel}
172
+ variant="secondary"
173
+ size="lg"
174
+ onPress={onBack}
175
+ />
151
176
  ) : null}
177
+ <View style={{ flex: 1, gap: 4 }}>
178
+ <Text variant="h3" weight="semibold" color="primary">
179
+ {title}
180
+ </Text>
181
+ {subtitle ? (
182
+ <Text variant="bodyLarge" color="secondary">
183
+ {subtitle}
184
+ </Text>
185
+ ) : null}
186
+ </View>
152
187
  </View>
153
188
  </View>
154
189
  );
155
190
 
156
- const contentBody = <View style={{ paddingHorizontal: padding }}>{children}</View>;
191
+ const hasButtons = cancel != null || actions.length > 0;
192
+ const hasFooter = hasButtons || footerAccessory != null;
193
+
194
+ // The footer owns the bottom padding; without a footer the content would
195
+ // otherwise hug the bottom edge, so it takes over the bottom padding itself.
196
+ const contentBody = (
197
+ <View style={{ paddingHorizontal: padding, paddingBottom: hasFooter ? 0 : padding }}>
198
+ {children}
199
+ </View>
200
+ );
157
201
 
158
- const hasFooter = cancel != null || actions.length > 0;
159
202
  const footer = hasFooter ? (
160
203
  <View
161
204
  style={{
162
- flexDirection: 'row',
163
- gap: 12,
164
205
  paddingHorizontal: padding,
165
206
  paddingBottom: padding,
166
207
  paddingTop: contentSpacing,
167
208
  }}
168
209
  >
169
- {cancel ? (
170
- <View style={{ flex: 1 }}>
171
- <Button
172
- fullWidth
173
- variant={cancel.destructive ? 'destructive' : 'secondary'}
174
- label={cancel.label ?? 'Cancel'}
175
- onPress={cancel.onPress ?? onClose}
176
- />
210
+ {footerAccessory ? (
211
+ <View
212
+ style={{
213
+ alignItems: 'center',
214
+ // Figma keeps the accessory (e.g. step progress) closer to the
215
+ // content above it than to the buttons below (~40 vs ~56).
216
+ marginBottom: hasButtons ? Math.round(contentSpacing * 1.4) : 0,
217
+ }}
218
+ >
219
+ {footerAccessory}
177
220
  </View>
178
221
  ) : null}
179
- {actions.map((action, i) => (
180
- <View key={i} style={{ flex: 1 }}>
181
- <Button
182
- fullWidth
183
- variant={action.variant ?? 'default'}
184
- label={action.label}
185
- onPress={action.onPress}
186
- loading={action.loading}
187
- disabled={action.disabled}
188
- />
222
+ {hasButtons ? (
223
+ <View style={{ flexDirection: 'row', gap: 12 }}>
224
+ {cancel ? (
225
+ <View style={{ flex: 1 }}>
226
+ <Button
227
+ fullWidth
228
+ variant={cancel.destructive ? 'destructive' : 'secondary'}
229
+ label={cancel.label ?? 'Cancel'}
230
+ onPress={cancel.onPress ?? onClose}
231
+ />
232
+ </View>
233
+ ) : null}
234
+ {actions.map((action, i) => (
235
+ <View key={i} style={{ flex: 1 }}>
236
+ <Button
237
+ fullWidth
238
+ variant={action.variant ?? 'default'}
239
+ label={action.label}
240
+ onPress={action.onPress}
241
+ loading={action.loading}
242
+ disabled={action.disabled}
243
+ />
244
+ </View>
245
+ ))}
189
246
  </View>
190
- ))}
247
+ ) : null}
191
248
  </View>
192
249
  ) : null;
193
250
 
@@ -213,6 +270,7 @@ export function Dialog({
213
270
  <ScrollArea
214
271
  indicatorColor={indicatorColor}
215
272
  style={{ flex: 1 }}
273
+ contentContainerStyle={{ paddingVertical: 8 }}
216
274
  keyboardShouldPersistTaps="handled"
217
275
  keyboardDismissMode="interactive"
218
276
  >
@@ -239,6 +297,7 @@ export function Dialog({
239
297
  <ScrollArea
240
298
  indicatorColor={indicatorColor}
241
299
  style={{ flexShrink: 1 }}
300
+ contentContainerStyle={{ paddingVertical: 8 }}
242
301
  keyboardShouldPersistTaps="handled"
243
302
  keyboardDismissMode="interactive"
244
303
  >
@@ -258,6 +317,7 @@ export function Dialog({
258
317
  statusBarTranslucent
259
318
  onRequestClose={onClose}
260
319
  >
320
+ <ModalHostContext.Provider value={true}>
261
321
  <KeyboardAvoidingView
262
322
  behavior={Platform.OS === 'ios' ? 'padding' : undefined}
263
323
  style={{ flex: 1 }}
@@ -294,6 +354,7 @@ export function Dialog({
294
354
  )}
295
355
  </View>
296
356
  </KeyboardAvoidingView>
357
+ </ModalHostContext.Provider>
297
358
  </Modal>
298
359
  );
299
360
  }
@@ -0,0 +1,173 @@
1
+ import { useEffect, useState, type ReactNode } from 'react';
2
+ import { ProgressBar } from '../ProgressBar/ProgressBar';
3
+ import { Dialog, type DialogProps } from './Dialog';
4
+
5
+ export interface StepDialogStep {
6
+ /** Header title for this step. */
7
+ title: string;
8
+ /** Optional subtitle under the title. */
9
+ subtitle?: string;
10
+ /** The step body. */
11
+ content: ReactNode;
12
+ /** Primary button label. Defaults to "Continue" (or `finishLabel` on the last step). */
13
+ nextLabel?: string;
14
+ /** Disable the primary button — e.g. until this step is valid. */
15
+ nextDisabled?: boolean;
16
+ /** Show a spinner on the primary button (e.g. while submitting). */
17
+ nextLoading?: boolean;
18
+ /**
19
+ * Run when the primary button is pressed on this step. Return (or resolve to)
20
+ * `false` to stay on the step — e.g. when validation fails. Anything else
21
+ * advances to the next step (or finishes on the last one).
22
+ */
23
+ onNext?: () => boolean | void | Promise<boolean | void>;
24
+ }
25
+
26
+ export interface StepDialogProps
27
+ extends Omit<
28
+ DialogProps,
29
+ 'title' | 'subtitle' | 'children' | 'actions' | 'cancel' | 'onBack' | 'footerAccessory'
30
+ > {
31
+ /** The ordered steps of the flow. */
32
+ steps: StepDialogStep[];
33
+ /** Controlled step index. Omit for uncontrolled. */
34
+ step?: number;
35
+ /** Initial step index (uncontrolled). Defaults to 0. Resets to this on close. */
36
+ defaultStep?: number;
37
+ /** Notified whenever the step changes. */
38
+ onStepChange?: (index: number) => void;
39
+ /** Called after the last step's `onNext` resolves (unless it returned `false`). */
40
+ onFinish?: () => void;
41
+ /** Primary label on the last step. Defaults to "Finish". */
42
+ finishLabel?: string;
43
+ /** Secondary label used to go back on steps after the first. Defaults to "Back". */
44
+ backLabel?: string;
45
+ /** Secondary label on the first step (closes the dialog). Defaults to "Cancel". */
46
+ cancelLabel?: string;
47
+ /** Show the round back button in the header from step 2 onward. Defaults to true. */
48
+ showHeaderBack?: boolean;
49
+ /** Show the footer secondary button (Back / Cancel). Defaults to true. */
50
+ showFooterBack?: boolean;
51
+ /** Show the progress bar in the footer. Defaults to true. */
52
+ showProgress?: boolean;
53
+ /**
54
+ * Width of the progress bar within the footer (centered). A number is px; a
55
+ * string is any RN dimension (e.g. `'62%'`). Defaults to `'62%'` — the narrower,
56
+ * centered bar from Figma (≈ 500 / 820 of the content width).
57
+ */
58
+ progressWidth?: number | `${number}%`;
59
+ }
60
+
61
+ const clamp = (n: number, min: number, max: number) => (n < min ? min : n > max ? max : n);
62
+
63
+ /**
64
+ * StepDialog — a multi-step (wizard) flow built on `Dialog`. Give it an array of
65
+ * `steps`; it tracks the current one, renders a round back button in the header
66
+ * from step 2 onward, a thin progress bar in the footer, and a
67
+ * secondary (Back / Cancel) + primary (Continue / Finish) button pair. A step's
68
+ * `onNext` can return `false` (sync or async) to block advancing after failed
69
+ * validation. Controlled via `step`/`onStepChange` or fully uncontrolled.
70
+ * Forwards all other `Dialog` props (width, fullscreen, scrollMode, …).
71
+ */
72
+ export function StepDialog({
73
+ steps,
74
+ step,
75
+ defaultStep = 0,
76
+ onStepChange,
77
+ onFinish,
78
+ finishLabel = 'Finish',
79
+ backLabel = 'Back',
80
+ cancelLabel = 'Cancel',
81
+ showHeaderBack = true,
82
+ showFooterBack = true,
83
+ showProgress = true,
84
+ progressWidth = '62%',
85
+ visible,
86
+ onClose,
87
+ ...dialogProps
88
+ }: StepDialogProps) {
89
+ const total = steps.length;
90
+ const [uncontrolled, setUncontrolled] = useState(defaultStep);
91
+ const isControlled = step !== undefined;
92
+ const index = clamp(isControlled ? step : uncontrolled, 0, Math.max(0, total - 1));
93
+
94
+ // Reset an uncontrolled flow to its first step whenever it is closed.
95
+ useEffect(() => {
96
+ if (!visible && !isControlled) setUncontrolled(defaultStep);
97
+ }, [visible, isControlled, defaultStep]);
98
+
99
+ const setIndex = (next: number) => {
100
+ const clamped = clamp(next, 0, total - 1);
101
+ if (!isControlled) setUncontrolled(clamped);
102
+ onStepChange?.(clamped);
103
+ };
104
+
105
+ const current = steps[index];
106
+ if (!current) return null;
107
+
108
+ const isFirst = index === 0;
109
+ const isLast = index === total - 1;
110
+
111
+ const goBack = () => {
112
+ if (!isFirst) setIndex(index - 1);
113
+ };
114
+
115
+ const advance = () => {
116
+ if (isLast) onFinish?.();
117
+ else setIndex(index + 1);
118
+ };
119
+
120
+ const goNext = () => {
121
+ const result = current.onNext?.();
122
+ // Await only a real promise, so sync steps advance synchronously.
123
+ if (result && typeof (result as Promise<unknown>).then === 'function') {
124
+ void (result as Promise<boolean | void>).then((r) => {
125
+ if (r !== false) advance();
126
+ });
127
+ return;
128
+ }
129
+ if (result === false) return;
130
+ advance();
131
+ };
132
+
133
+ const footerAccessory = showProgress ? (
134
+ <ProgressBar
135
+ progress={(index + 1) / total}
136
+ showSupporting={false}
137
+ bordered={false}
138
+ height={6}
139
+ style={{ width: progressWidth, alignSelf: 'center' }}
140
+ accessibilityLabel={`Step ${index + 1} of ${total}`}
141
+ />
142
+ ) : undefined;
143
+
144
+ return (
145
+ <Dialog
146
+ {...dialogProps}
147
+ visible={visible}
148
+ onClose={onClose}
149
+ title={current.title}
150
+ subtitle={current.subtitle}
151
+ onBack={!isFirst && showHeaderBack ? goBack : undefined}
152
+ backAccessibilityLabel={backLabel}
153
+ footerAccessory={footerAccessory}
154
+ cancel={
155
+ showFooterBack
156
+ ? isFirst
157
+ ? { label: cancelLabel, onPress: onClose }
158
+ : { label: backLabel, onPress: goBack }
159
+ : undefined
160
+ }
161
+ actions={[
162
+ {
163
+ label: current.nextLabel ?? (isLast ? finishLabel : 'Continue'),
164
+ onPress: goNext,
165
+ disabled: current.nextDisabled,
166
+ loading: current.nextLoading,
167
+ },
168
+ ]}
169
+ >
170
+ {current.content}
171
+ </Dialog>
172
+ );
173
+ }
@@ -4,3 +4,4 @@ export {
4
4
  type DialogAction,
5
5
  type DialogCancelConfig,
6
6
  } from './Dialog';
7
+ export { StepDialog, type StepDialogProps, type StepDialogStep } from './StepDialog';