@ornikar/bumper 3.7.0 → 3.7.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/CHANGELOG.md CHANGED
@@ -3,6 +3,14 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [3.7.1](https://github.com/ornikar/kitt/compare/@ornikar/bumper@3.7.0...@ornikar/bumper@3.7.1) (2026-03-23)
7
+
8
+ **Note:** Version bump only for package @ornikar/bumper
9
+
10
+
11
+
12
+
13
+
6
14
  ## [3.7.0](https://github.com/ornikar/kitt/compare/@ornikar/bumper@3.6.2...@ornikar/bumper@3.7.0) (2026-03-17)
7
15
 
8
16
 
@@ -0,0 +1,502 @@
1
+ # Button → Button Migration Guide
2
+
3
+ > Source: `@ornikar/kitt-universal/Button`
4
+ > Target: `@ornikar/bumper/Button`
5
+ > Generated: 2026-03-20
6
+
7
+ ## Import Change
8
+
9
+ ```tsx
10
+ // Before
11
+ import { Button } from '@ornikar/kitt-universal';
12
+ import type { ButtonProps } from '@ornikar/kitt-universal';
13
+
14
+ // After
15
+ import { Button } from '@ornikar/bumper';
16
+ import type { ButtonProps } from '@ornikar/bumper';
17
+ ```
18
+
19
+ Also remove any `LoaderIcon` import used solely for Button loading patterns — bumper has a native `isLoading` prop.
20
+
21
+ ## Props Mapping
22
+
23
+ | Source Prop | Target Prop | Transform | Notes |
24
+ | ----------------------------------------------------------------------- | --------------------------------------- | -------------------------------------- | ----------------------------------------------------------------- |
25
+ | `children` (direct) | `<Button.Text>{children}</Button.Text>` | Wrap in compound component | See [Children & Composition](#children--composition) |
26
+ | `type="primary"` | `type="primary"` | Keep as-is | Same value |
27
+ | `type="secondary"` | `type="secondary"` | Keep as-is | Same value (also the default) |
28
+ | `type="tertiary"` | `type="tertiary"` | Keep as-is | Same value |
29
+ | `type="tertiary-danger"` | `type="danger"` | Rename value | Not used in learner-apps |
30
+ | `variant="default"` | — | Remove prop | Default behavior, no equivalent needed |
31
+ | `variant="revert"` | `isOnContrasted` | Replace prop + value with boolean prop | See [Variant Mapping](#variant-mapping) |
32
+ | `size="default"` | — | Remove prop | Maps to `large` which is the default |
33
+ | `size="medium"` | — | Remove prop | Maps to `large` which is the default |
34
+ | `disabled` | `disabled` | Keep as-is | Same API |
35
+ | `stretch` (boolean) | `stretch` | Keep as-is | Same API when static boolean |
36
+ | `stretch` (responsive object) | `stretch` + `$medium`/`$small`/etc. | Convert responsive syntax | See [Responsive Stretch](#responsive-stretch) |
37
+ | `icon={<Icon />}` | `<Button.Icon icon={<Icon />} />` | Convert prop to compound child | See [Icon Migration](#icon-migration) |
38
+ | `iconPosition="left"` | — | Remove prop | Icon position determined by child order (Icon before Text = left) |
39
+ | `iconPosition="right"` | — | Remove prop | Place `Button.Icon` after `Button.Text` |
40
+ | `icon={isLoading ? <LoaderIcon /> : <Icon />}` + `disabled={isLoading}` | `isLoading` | Replace pattern with single prop | See [Loading Pattern](#loading-pattern) |
41
+ | `withBadge` | `<Button.Badge />` | Convert prop to compound child | Not used in learner-apps |
42
+ | `badgeCount={n}` | `<Button.Badge count={n} />` | Convert prop to compound child | Not used in learner-apps |
43
+ | `timerAttrs` | — | **Not migratable** | No bumper equivalent. Requires manual review |
44
+ | `testID` | `testID` | Keep as-is | Same API |
45
+ | `onPress` | `onPress` | Keep as-is | Same API |
46
+ | `onFocus` | — | Remove prop | Not exposed in bumper ButtonProps |
47
+ | `onBlur` | — | Remove prop | Not exposed in bumper ButtonProps |
48
+ | `onHoverIn` | — | Remove prop | Not exposed in bumper ButtonProps |
49
+ | `onHoverOut` | — | Remove prop | Not exposed in bumper ButtonProps |
50
+ | `href` | — | Remove prop | Not used in learner-apps. Navigation uses InternalLink wrappers |
51
+ | `hrefAttrs` | — | Remove prop | Not used in learner-apps |
52
+ | `accessibilityRole` | — | Remove prop | Bumper sets `role="button"` automatically |
53
+ | `innerSpacing` | — | Remove prop | Not used in learner-apps |
54
+ | `style` | — | Remove prop | Not supported. Use Tamagui style props if needed |
55
+ | `isHoveredInternal` | — | Remove prop | Internal/storybook only |
56
+ | `isPressedInternal` | — | Remove prop | Internal/storybook only |
57
+ | `isFocusedInternal` | — | Remove prop | Internal/storybook only |
58
+
59
+ ## Value Mappings
60
+
61
+ ### Type Values
62
+
63
+ ```
64
+ primary → primary
65
+ secondary → secondary (default in both)
66
+ tertiary → tertiary
67
+ tertiary-danger → danger (not used in learner-apps)
68
+ ```
69
+
70
+ ### Size Values
71
+
72
+ All sizes map to `large` (the default), so remove the `size` prop entirely:
73
+
74
+ ```
75
+ default → (remove prop) — large is the default
76
+ medium → (remove prop) — large is the default
77
+ ```
78
+
79
+ ### Variant Mapping
80
+
81
+ ```
82
+ variant="default" → (remove prop entirely)
83
+ variant="revert" → isOnContrasted
84
+ ```
85
+
86
+ When `variant="revert"` also remove the `variant` prop itself:
87
+
88
+ ```tsx
89
+ // Before
90
+ <Button type="primary" variant="revert" onPress={onClose}>
91
+
92
+ // After
93
+ <Button type="primary" isOnContrasted onPress={onClose}>
94
+ ```
95
+
96
+ ## Children & Composition
97
+
98
+ The biggest change: bumper Button uses a **compound component pattern**. All text children must be wrapped in `Button.Text`.
99
+
100
+ ### Basic text children
101
+
102
+ ```tsx
103
+ // Before
104
+ <Button type="primary" onPress={onPress}>
105
+ <FormattedMessage defaultMessage="Continue" />
106
+ </Button>
107
+
108
+ // After
109
+ <Button type="primary" onPress={onPress}>
110
+ <Button.Text>
111
+ <FormattedMessage defaultMessage="Continue" />
112
+ </Button.Text>
113
+ </Button>
114
+ ```
115
+
116
+ ### Multiple children / mixed content
117
+
118
+ If the source Button has multiple children (e.g., text nodes), wrap them all in a single `Button.Text`:
119
+
120
+ ```tsx
121
+ // Before
122
+ <Button type="primary" onPress={onPress}>
123
+ {label}
124
+ </Button>
125
+
126
+ // After
127
+ <Button type="primary" onPress={onPress}>
128
+ <Button.Text>{label}</Button.Text>
129
+ </Button>
130
+ ```
131
+
132
+ ### Icon-only button (no text children)
133
+
134
+ If the source Button has only an `icon` prop and no `children`, use only `Button.Icon`:
135
+
136
+ ```tsx
137
+ // Before
138
+ <Button icon={<ReplayIcon />} type="primary" onPress={onReplay} />
139
+
140
+ // After
141
+ <Button type="primary" onPress={onReplay}>
142
+ <Button.Icon icon={<ReplayIcon />} />
143
+ </Button>
144
+ ```
145
+
146
+ **Note**: Consider migrating icon-only buttons to `IconButton` from bumper instead, depending on design intent.
147
+
148
+ ## Icon Migration
149
+
150
+ ### Icon with text (left position — default)
151
+
152
+ ```tsx
153
+ // Before
154
+ <Button type="primary" icon={<ArrowRightIcon />} onPress={onNext}>
155
+ <FormattedMessage defaultMessage="Next" />
156
+ </Button>
157
+
158
+ // After
159
+ <Button type="primary" onPress={onNext}>
160
+ <Button.Icon icon={<ArrowRightIcon />} />
161
+ <Button.Text>
162
+ <FormattedMessage defaultMessage="Next" />
163
+ </Button.Text>
164
+ </Button>
165
+ ```
166
+
167
+ ### Icon with text (right position)
168
+
169
+ Place `Button.Icon` **after** `Button.Text`:
170
+
171
+ ```tsx
172
+ // Before
173
+ <Button icon={<ArrowRightIcon />} iconPosition="right" onPress={onNext}>
174
+ <FormattedMessage defaultMessage="Next" />
175
+ </Button>
176
+
177
+ // After
178
+ <Button onPress={onNext}>
179
+ <Button.Text>
180
+ <FormattedMessage defaultMessage="Next" />
181
+ </Button.Text>
182
+ <Button.Icon icon={<ArrowRightIcon />} />
183
+ </Button>
184
+ ```
185
+
186
+ ## Loading Pattern
187
+
188
+ The pervasive `LoaderIcon` + `disabled` pattern in kitt-universal maps to bumper's native `isLoading` prop:
189
+
190
+ ```tsx
191
+ // Before
192
+ <Button
193
+ type="primary"
194
+ icon={isLoading ? <LoaderIcon /> : <ArrowRightIcon />}
195
+ disabled={isLoading}
196
+ onPress={handleSubmit}
197
+ >
198
+ <FormattedMessage defaultMessage="Submit" />
199
+ </Button>
200
+
201
+ // After
202
+ <Button type="primary" isLoading={isLoading} onPress={handleSubmit}>
203
+ <Button.Icon icon={<ArrowRightIcon />} />
204
+ <Button.Text>
205
+ <FormattedMessage defaultMessage="Submit" />
206
+ </Button.Text>
207
+ </Button>
208
+ ```
209
+
210
+ **Important**: When `isLoading` is true, bumper hides all children (with `opacity: 0`) and shows a centered spinner. The button preserves its original dimensions. There is no need to conditionally swap icon elements.
211
+
212
+ If the source only uses `LoaderIcon` without a fallback icon (icon-only loading):
213
+
214
+ ```tsx
215
+ // Before
216
+ <Button icon={isLoading ? <LoaderIcon /> : undefined} disabled={isLoading} onPress={onPress}>
217
+ <FormattedMessage defaultMessage="Action" />
218
+ </Button>
219
+
220
+ // After
221
+ <Button isLoading={isLoading} onPress={onPress}>
222
+ <Button.Text>
223
+ <FormattedMessage defaultMessage="Action" />
224
+ </Button.Text>
225
+ </Button>
226
+ ```
227
+
228
+ If `disabled` is used for reasons **beyond** loading (e.g., form validation), keep it alongside `isLoading`:
229
+
230
+ ```tsx
231
+ // Before
232
+ <Button
233
+ disabled={isLoading || !isFormValid}
234
+ icon={isLoading ? <LoaderIcon /> : <CheckIcon />}
235
+ onPress={onSubmit}
236
+ >
237
+ Submit
238
+ </Button>
239
+
240
+ // After
241
+ <Button
242
+ isLoading={isLoading}
243
+ disabled={!isFormValid}
244
+ onPress={onSubmit}
245
+ >
246
+ <Button.Icon icon={<CheckIcon />} />
247
+ <Button.Text>Submit</Button.Text>
248
+ </Button>
249
+ ```
250
+
251
+ ## Responsive Stretch
252
+
253
+ kitt-universal uses `ResponsiveValue<boolean>` objects. Bumper uses Tamagui media query props.
254
+
255
+ ### Breakpoint name mapping
256
+
257
+ ```
258
+ base → (base prop, no prefix)
259
+ small → $small
260
+ medium → $medium
261
+ large → $large
262
+ ```
263
+
264
+ ### Conversion rules
265
+
266
+ The base value becomes the direct prop. Override values become media query props.
267
+
268
+ ```tsx
269
+ // Before: stretch={{ base: true, small: false }}
270
+ // After:
271
+ <Button stretch $small={{ stretch: false }}>
272
+
273
+ // Before: stretch={{ base: true, large: false }}
274
+ // After:
275
+ <Button stretch $large={{ stretch: false }}>
276
+
277
+ // Before: stretch={{ base: true, medium: false }}
278
+ // After:
279
+ <Button stretch $medium={{ stretch: false }}>
280
+
281
+ // Before: stretch={true}
282
+ // After:
283
+ <Button stretch>
284
+
285
+ // Before: stretch={false} or stretch={undefined}
286
+ // After:
287
+ // (remove the prop entirely)
288
+ ```
289
+
290
+ ### Dynamic responsive values
291
+
292
+ If `stretch` receives a variable (not an object literal), it cannot be mechanically converted. Flag for manual review:
293
+
294
+ ```tsx
295
+ // Before
296
+ <Button stretch={responsiveStretch} onPress={onPress}>
297
+
298
+ // After — MANUAL REVIEW NEEDED
299
+ // Must convert ResponsiveValue<boolean> to Tamagui media query props
300
+ ```
301
+
302
+ ## Behavioral Differences
303
+
304
+ | Aspect | kitt-universal | bumper |
305
+ | ----------------------------- | --------------------------------------------- | --------------------------------------------- | ----------------------------------------------- |
306
+ | **Default type** | `secondary` | `secondary` | Same |
307
+ | **Default size** | `default` (40px min-height) | `large` (48px) | Slightly larger |
308
+ | **Border radius** | 4px | `$radius.m` token | May differ visually |
309
+ | **Focus ring** | 3px purple border via `FocusBorder` component | 3px outline via `outlineColor: $border.focus` | Similar but implemented differently |
310
+ | **Loading** | No built-in loading | `isLoading` prop with spinner | New capability |
311
+ | **Animation** | `react-native-reanimated` spring animations | Tamagui built-in transitions | Different animation library |
312
+ | **Gap between icon and text** | Theme-dependent spacing | Fixed `$space.8` (8px) gap | May differ slightly |
313
+ | **Icon sizing** | Size depends on button size (20px/24px) | Fixed `$icon.m` | Consistent across sizes |
314
+ | **Children required** | Either `children` or `icon` required | `children` is required | Must always have children (compound components) |
315
+
316
+ ## Edge Cases
317
+
318
+ ### Conditional type values
319
+
320
+ Map each possible value:
321
+
322
+ ```tsx
323
+ // Before
324
+ <Button type={isPrimary ? "primary" : "tertiary"} onPress={onPress}>
325
+
326
+ // After
327
+ <Button type={isPrimary ? "primary" : "tertiary"} onPress={onPress}>
328
+ // Same — these values map 1:1
329
+ ```
330
+
331
+ ### Conditional variant
332
+
333
+ ```tsx
334
+ // Before
335
+ <Button variant={isContrasted ? "revert" : "default"} onPress={onPress}>
336
+
337
+ // After
338
+ <Button isOnContrasted={isContrasted} onPress={onPress}>
339
+ ```
340
+
341
+ ### Spread props from ButtonProps
342
+
343
+ If components spread ButtonProps, filter out removed props:
344
+
345
+ ```tsx
346
+ // Before
347
+ const { timerAttrs, icon, iconPosition, variant, size, ...rest } = buttonProps;
348
+
349
+ // After — remap relevant props
350
+ const {
351
+ timerAttrs,
352
+ icon,
353
+ iconPosition,
354
+ variant,
355
+ size,
356
+ onFocus,
357
+ onBlur,
358
+ onHoverIn,
359
+ onHoverOut,
360
+ style,
361
+ href,
362
+ hrefAttrs,
363
+ accessibilityRole,
364
+ innerSpacing,
365
+ withBadge,
366
+ badgeCount,
367
+ ...rest
368
+ } = buttonProps;
369
+ <Button {...rest} isOnContrasted={variant === 'revert'}>
370
+ {icon && <Button.Icon icon={icon} />}
371
+ {children && <Button.Text>{children}</Button.Text>}
372
+ </Button>;
373
+ ```
374
+
375
+ ### Wrapper / re-export migration
376
+
377
+ Components that extend or pick from `ButtonProps` need type updates:
378
+
379
+ ```tsx
380
+ // Before
381
+ import type { ButtonProps } from '@ornikar/kitt-universal';
382
+ interface MyButtonProps extends Pick<ButtonProps, 'stretch' | 'type'> { ... }
383
+
384
+ // After
385
+ import type { ButtonProps } from '@ornikar/bumper';
386
+ interface MyButtonProps extends Pick<ButtonProps, 'stretch' | 'type'> { ... }
387
+ // stretch and type exist in both — no further changes needed
388
+ ```
389
+
390
+ For wrappers that pick `icon`, `variant`, or `size`:
391
+
392
+ ```tsx
393
+ // Before
394
+ interface MyProps extends Pick<ButtonProps, 'icon' | 'variant' | 'size'> { ... }
395
+
396
+ // After — these props no longer exist on ButtonProps
397
+ // icon → render Button.Icon in the component body
398
+ // variant → replace with isOnContrasted
399
+ // size → remove (always large)
400
+ interface MyProps {
401
+ icon?: ReactNode;
402
+ isOnContrasted?: boolean;
403
+ }
404
+ ```
405
+
406
+ ### ActionButton wrapper (native)
407
+
408
+ The `ActionButton` wrapper that auto-shows `LoaderIcon` during async `onPress` can be simplified:
409
+
410
+ ```tsx
411
+ // Before (ActionButton pattern)
412
+ <ActionButton type="primary" icon={<ArrowRightIcon />} onPress={asyncHandler}>
413
+ <FormattedMessage defaultMessage="Submit" />
414
+ </ActionButton>;
415
+
416
+ // After — use isLoading directly
417
+ const [isLoading, setIsLoading] = useState(false);
418
+ <Button type="primary" isLoading={isLoading} onPress={wrappedHandler}>
419
+ <Button.Icon icon={<ArrowRightIcon />} />
420
+ <Button.Text>
421
+ <FormattedMessage defaultMessage="Submit" />
422
+ </Button.Text>
423
+ </Button>;
424
+ ```
425
+
426
+ ### InternalLink / InternalLinkButton wrappers
427
+
428
+ These wrap Button for navigation. Update the Button props inside:
429
+
430
+ ```tsx
431
+ // Before
432
+ <InternalLink as={Button} to={route} type="tertiary" icon={<ArrowLeftIcon />}>
433
+ <FormattedMessage defaultMessage="Back" />
434
+ </InternalLink>
435
+
436
+ // After — Button props change but InternalLink pattern stays
437
+ // NOTE: InternalLink's `as` prop may need updating if it expects specific ButtonProps shape.
438
+ // Flag for manual review if the wrapper doesn't support compound children.
439
+ ```
440
+
441
+ ### Actions.Button usage
442
+
443
+ `Actions.Button` from kitt-universal shares the same `ButtonProps` interface but lives inside the `Actions` compound component. These should be migrated **separately** as part of an `Actions` migration. Do not convert `Actions.Button` as part of this Button migration.
444
+
445
+ ## Migration Rules (machine-readable)
446
+
447
+ Apply these rules in order for each `<Button>` usage:
448
+
449
+ 1. **Update import**: Replace `'@ornikar/kitt-universal'` with `'@ornikar/bumper'` for `Button` and `ButtonProps` imports. Remove `LoaderIcon` import if only used for Button loading patterns.
450
+
451
+ 2. **Remove `size` prop**: Delete any `size="default"` or `size="medium"` — bumper defaults to `large`.
452
+
453
+ 3. **Convert `variant`**:
454
+
455
+ - If `variant="default"` → remove the prop
456
+ - If `variant="revert"` → replace with `isOnContrasted`
457
+ - If `variant={expr}` → replace with `isOnContrasted={expr === 'revert'}`
458
+
459
+ 4. **Convert `type` values**:
460
+
461
+ - `"tertiary-danger"` → `"danger"`
462
+ - All others unchanged
463
+
464
+ 5. **Convert `stretch` responsive syntax**:
465
+
466
+ - If `stretch={true}` or `stretch` → keep as `stretch`
467
+ - If `stretch={{ base: X, breakpoint: Y }}` → convert base to prop, breakpoints to `$breakpoint={{ stretch: Y }}`
468
+ - If `stretch={variable}` → flag for manual review
469
+
470
+ 6. **Detect loading pattern**: If the JSX contains `icon={condition ? <LoaderIcon /> : <OtherIcon />}` paired with `disabled={sameCondition}`:
471
+
472
+ - Add `isLoading={condition}` prop
473
+ - Remove the `LoaderIcon` conditional from icon
474
+ - Remove `disabled={condition}` (keep `disabled` if it has additional conditions)
475
+ - Move the non-LoaderIcon icon to `<Button.Icon icon={...} />`
476
+
477
+ 7. **Convert `icon` prop to compound child**:
478
+
479
+ - Remove `icon` prop from `<Button>`
480
+ - If `iconPosition="right"` or `iconPosition` is absent/`"left"` → determines child order
481
+ - Add `<Button.Icon icon={iconValue} />` as child (before `Button.Text` for left, after for right)
482
+ - Remove `iconPosition` prop
483
+
484
+ 8. **Wrap text children in `Button.Text`**:
485
+
486
+ - Take all remaining children of `<Button>` (text, `<FormattedMessage>`, expressions)
487
+ - Wrap them in `<Button.Text>...</Button.Text>`
488
+ - If Button had no `children` (icon-only), skip this step
489
+
490
+ 9. **Remove unsupported props**: Delete `onFocus`, `onBlur`, `onHoverIn`, `onHoverOut`, `href`, `hrefAttrs`, `accessibilityRole`, `innerSpacing`, `style`, `isHoveredInternal`, `isPressedInternal`, `isFocusedInternal`, `withBadge`, `badgeCount`.
491
+
492
+ 10. **Self-closing to explicit children**: If the source was a self-closing `<Button ... />`, convert to `<Button ...>...</Button>` with compound children inside.
493
+
494
+ ## Not Migratable (requires human review)
495
+
496
+ - **`timerAttrs` prop** (1 usage in `AnswerSection`): No bumper equivalent. The timer/gauge overlay feature does not exist in bumper Button. This usage needs a custom solution or feature request.
497
+ - **Dynamic `stretch` variables**: When `stretch` receives a runtime variable (not an object literal), ask the user on how to proceed.
498
+ - **`InternalLink as={Button}` wrappers**: The `as` polymorphic pattern may not be compatible with bumper's compound children model. Each InternalLink/InternalLinkButton wrapper needs manual verification.
499
+ - **`Actions.Button`**: Shares ButtonProps but lives in a different component tree. Migrate separately as part of Actions component migration.
500
+ - **Icon-only buttons**: Don't do anything. Migrate separatly as part of the IconButton component migration.
501
+ - **`style` prop usage**: Any custom styles passed via `style` prop need manual conversion to Tamagui style props.
502
+ - **Components extending `ButtonProps`**: Proceed with the changes, only ask for user input if the linting is failing.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ornikar/bumper",
3
- "version": "3.7.0",
3
+ "version": "3.7.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "directory": "@ornikar/bumper",