@ornikar/bumper 3.10.0 → 3.10.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 (54) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/definitions/shared/storybook/StorySection.d.ts.map +1 -1
  3. package/dist/definitions/shared/storybook/StoryTitle.d.ts.map +1 -1
  4. package/dist/definitions/system/actions/Button/Button.d.ts +4 -4
  5. package/dist/definitions/system/actions/Button/Button.d.ts.map +1 -1
  6. package/dist/definitions/system/actions/IconButton/IconButton.d.ts.map +1 -1
  7. package/dist/definitions/system/content/icon/Icon.d.ts.map +1 -1
  8. package/dist/definitions/system/content/typography/Typography.d.ts +15 -10
  9. package/dist/definitions/system/content/typography/Typography.d.ts.map +1 -1
  10. package/dist/definitions/system/content/typography/TypographyLink.d.ts +1 -0
  11. package/dist/definitions/system/content/typography/TypographyLink.d.ts.map +1 -1
  12. package/dist/definitions/system/loading/loader/Loader.d.ts +2 -2
  13. package/dist/definitions/system/loading/loader/Loader.d.ts.map +1 -1
  14. package/dist/definitions/system/loading/loader/LoaderBackgroundCircle.d.ts +2 -1
  15. package/dist/definitions/system/loading/loader/LoaderBackgroundCircle.d.ts.map +1 -1
  16. package/dist/definitions/system/loading/loader/LoaderCircleWrapper.d.ts +2 -1
  17. package/dist/definitions/system/loading/loader/LoaderCircleWrapper.d.ts.map +1 -1
  18. package/dist/definitions/system/types.d.ts +38 -1
  19. package/dist/definitions/system/types.d.ts.map +1 -1
  20. package/dist/index-metro.es.android.js +3 -2
  21. package/dist/index-metro.es.android.js.map +1 -1
  22. package/dist/index-metro.es.ios.js +3 -2
  23. package/dist/index-metro.es.ios.js.map +1 -1
  24. package/dist/index-node-22.22.cjs.js +3 -2
  25. package/dist/index-node-22.22.cjs.js.map +1 -1
  26. package/dist/index-node-22.22.cjs.web.js +3 -2
  27. package/dist/index-node-22.22.cjs.web.js.map +1 -1
  28. package/dist/index-node-22.22.es.mjs +3 -2
  29. package/dist/index-node-22.22.es.mjs.map +1 -1
  30. package/dist/index-node-22.22.es.web.mjs +3 -2
  31. package/dist/index-node-22.22.es.web.mjs.map +1 -1
  32. package/dist/index.es.js +3 -2
  33. package/dist/index.es.js.map +1 -1
  34. package/dist/index.es.web.js +3 -2
  35. package/dist/index.es.web.js.map +1 -1
  36. package/dist/tsbuildinfo +1 -1
  37. package/docs/migration/Typography.md +74 -8
  38. package/package.json +2 -2
  39. package/src/.eslintrc.json +2 -5
  40. package/src/shared/storybook/StorySection.tsx +8 -1
  41. package/src/shared/storybook/StoryTitle.tsx +8 -2
  42. package/src/system/actions/Button/Button.tsx +4 -2
  43. package/src/system/actions/IconButton/IconButton.tsx +3 -2
  44. package/src/system/content/icon/Icon.tsx +7 -2
  45. package/src/system/content/typography/Typography.features.stories.tsx +2 -2
  46. package/src/system/content/typography/Typography.stories.tsx +2 -2
  47. package/src/system/content/typography/Typography.tsx +21 -19
  48. package/src/system/content/typography/TypographyLink.features.stories.tsx +2 -2
  49. package/src/system/content/typography/TypographyLink.stories.tsx +2 -2
  50. package/src/system/content/typography/TypographyLink.tsx +4 -2
  51. package/src/system/loading/loader/Loader.tsx +16 -15
  52. package/src/system/loading/loader/LoaderBackgroundCircle.tsx +8 -1
  53. package/src/system/loading/loader/LoaderCircleWrapper.tsx +11 -1
  54. package/src/system/types.ts +51 -2
@@ -58,11 +58,13 @@ import { Typography } from '@ornikar/bumper';
58
58
 
59
59
  ### TypographyIcon → Typography.Icon
60
60
 
61
- | Source Prop | Target Prop | Transform | Notes |
62
- | ----------- | ----------- | --------------- | ------------------------------------------------------------------------------------- |
63
- | `icon` | `icon` | Keep as-is | Same API |
64
- | `color` | `color` | Map color value | See [Color Mapping](#color-mapping); `"inherit"` value removed — omit prop to inherit |
65
- | `size` | `size` | Map to token | Numeric values → `$icon.s` (16) / `$icon.m` (20) / `$icon.l` (24) |
61
+ | Source Prop | Target Prop | Transform | Notes |
62
+ | ----------- | ----------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
63
+ | `icon` | `icon` | Keep as-is | Same API |
64
+ | `color` | `color` | Map color value | See [Color Mapping](#color-mapping); `"inherit"` value removed — omit prop to inherit |
65
+ | `size` | `size` | Map to token | `16` → `$icon.s`, `20` `$icon.m`, `24` `$icon.l`. **Any other numeric value (e.g. `40`, `32`) has no bumper token** — see [Unsupported icon sizes](#unsupported-icon-sizes). |
66
+ | `align` | `alignSelf` | Rename prop | `align="center"` → `alignSelf="center"` |
67
+ | `testID` | `testID` | Keep as-is | Same API |
66
68
 
67
69
  ## Value Mappings
68
70
 
@@ -150,10 +152,12 @@ These are the most common colors found in the codebase. They need the `kitt.bump
150
152
 
151
153
  ```
152
154
  16 → "$icon.s"
153
- 20 → "$icon.m"
155
+ 20 → "$icon.m" (kitt-universal default when size is omitted)
154
156
  24 → "$icon.l"
155
157
  ```
156
158
 
159
+ **Unsupported sizes**: bumper's icon tokens only cover `s` / `m` / `l`. Sizes such as `40` and `32` have no equivalent token (`$icon.xl` does not exist — see [`bumperIcon.ts`](../../src/system/core/tokens/bumperIcon.ts)). Leave such call sites on `@ornikar/kitt-universal`'s `Icon` / `TypographyIcon`, or flag with a `// TODO: bumper has no matching icon size` comment — do not auto-migrate.
160
+
157
161
  ## Sub-Component Mapping
158
162
 
159
163
  | Source | Target | Notes |
@@ -311,10 +315,12 @@ Key differences:
311
315
  5. **No SetDefaultColor**: Target has no context provider for default colors. Colors must be set on each component individually.
312
316
  6. **No TypographyEmoji**: Target has no emoji sub-component.
313
317
  7. **Platform props syntax**: `_web` → `$platform-web`, `_ios`/`_android` → `$platform-native`. Note that both `_ios` and `_android` map to the same `$platform-native` prop — Tamagui has no per-platform native distinction. If the source used different values for iOS and Android, they must be reconciled manually.
314
- 8. **Color inheritance**: Both support color inheritance via context, but the context APIs differ internally. Behavior is equivalent.
318
+ 8. **Color inheritance**: When a `Typography.Text` is nested inside another bumper `Typography.*`, color propagates automatically via Tamagui's styled context — omit the `color` prop on the child and it will inherit the parent's value. However, inheritance **does NOT work** when the ancestor is a kitt-universal component that provides color via its own context (e.g., `Button`, `RadioGroup`, `CustomTooltip`). In those cases, **do not migrate** the `Typography.Text` — leave it on `@ornikar/kitt-universal`. See [Color inheritance from parent context](#color-inheritance-from-parent-context).
315
319
  9. **Responsive API**: Source uses breakpoint props (`base`, `medium`, `large`) or `type` object. Target uses Tamagui media props (`$small`, `$medium`, `$large`).
316
320
  10. **TypographyLink/TypographyIcon**: Source exports as standalone components. Target uses compound component pattern (`Typography.Link`, `Typography.Icon`).
317
321
  11. **Icon color="inherit"**: Source supports `color="inherit"`. Target does not — simply omit the color prop to inherit from parent.
322
+ 12. **Icon `align` prop renamed**: Source uses `align`; target uses `alignSelf`.
323
+ 13. **Icon tokens only cover s/m/l**: Source accepts any numeric `size`; target only has `$icon.s` (16), `$icon.m` (20), `$icon.l` (24). Other values have no equivalent.
318
324
 
319
325
  ## Edge Cases
320
326
 
@@ -458,6 +464,34 @@ const color = getTypographyColor(state); // should return '$content.accent', '$c
458
464
  <Typography.Text color={color}>text</Typography.Text>;
459
465
  ```
460
466
 
467
+ ### Color inheritance from parent context
468
+
469
+ **Inheritance works automatically** when the parent is a bumper `Typography.*`. Tamagui's styled context propagates the color to descendants — omit the `color` prop on the child:
470
+
471
+ ```tsx
472
+ // Parent sets the color; child inherits via Tamagui context
473
+ <Typography.Text color="$content.accent">
474
+ Headline with <Typography.Text weight="bold">bold</Typography.Text> inside
475
+ </Typography.Text>
476
+ ```
477
+
478
+ **Inheritance does NOT work** when the ancestor is a kitt-universal component that provides color via its own (non-Tamagui) context. bumper's `Typography.Text` applies its default `$content.base.hi` and has no reliable way to read those foreign contexts.
479
+
480
+ **Skip the migration entirely** in these cases — leave the `Typography.*` on `@ornikar/kitt-universal`. Known problematic ancestors:
481
+
482
+ - `Button` (any variant that renders on a contrasted surface)
483
+ - `RadioGroup`
484
+ - `CustomTooltip`
485
+
486
+ ```tsx
487
+ // ❌ DO NOT migrate — Button provides color via kitt's context, bumper cannot inherit it
488
+ <Button type="primary">
489
+ <Typography.Text>Action</Typography.Text> {/* keep on @ornikar/kitt-universal */}
490
+ </Button>
491
+ ```
492
+
493
+ If you cannot leave the call site on kitt-universal (e.g. mixed-imports are undesirable), the only safe escape hatch is to pass the correct bumper token explicitly — but prefer skipping the migration over guessing the token.
494
+
461
495
  ### TypographyIcon with color="inherit"
462
496
 
463
497
  ```tsx
@@ -468,6 +502,31 @@ const color = getTypographyColor(state); // should return '$content.accent', '$c
468
502
  <Typography.Icon icon={<CheckIcon />} />
469
503
  ```
470
504
 
505
+ ### TypographyIcon with `align`
506
+
507
+ ```tsx
508
+ // Before — kitt-universal supports align
509
+ <TypographyIcon icon={<LoaderIcon />} align="center" />
510
+
511
+ // After — bumper renames to alignSelf
512
+ <Typography.Icon icon={<LoaderIcon />} alignSelf="center" />
513
+ ```
514
+
515
+ ### Unsupported icon sizes
516
+
517
+ `bumper`'s icon tokens only cover `s` (16) / `m` (20) / `l` (24). Any other numeric size has no equivalent token.
518
+
519
+ ```tsx
520
+ // Before
521
+ <TypographyIcon icon={<AvatarIcon />} size={40} />
522
+
523
+ // After — DO NOT auto-migrate. Either:
524
+ // (a) leave the call site on kitt-universal's TypographyIcon, or
525
+ // (b) flag for manual review:
526
+ // TODO: bumper has no matching icon size for 40
527
+ <TypographyIcon icon={<AvatarIcon />} size={40} />
528
+ ```
529
+
471
530
  ## Migration Rules (machine-readable)
472
531
 
473
532
  1. **Update import**: Replace `import { Typography } from '@ornikar/kitt-universal'` with `import { Typography } from '@ornikar/bumper'`
@@ -492,7 +551,12 @@ const color = getTypographyColor(state); // should return '$content.accent', '$c
492
551
  20. **Map color values**: Apply color mapping table (e.g. `"black"` → `"$content.base.hi"`, `"kitt.bumper.X"` → `"$X"`)
493
552
  21. **Remove weight on headings/labels**: If target `variant` is a heading or label type, remove the `weight` prop (it's fixed)
494
553
  22. **Icon color="inherit"**: Remove `color="inherit"` prop (omitting color achieves inheritance)
495
- 23. **Icon size**: Map numeric sizes to tokens (`16` → `"$icon.s"`, `20` → `"$icon.m"`, `24` → `"$icon.l"`)
554
+ 23. **Icon size**: Map numeric sizes to tokens (`16` → `"$icon.s"`, `20` → `"$icon.m"`, `24` → `"$icon.l"`). **Omitted `size`** on the source (kitt-universal defaults to 20) also maps to `"$icon.m"`.
555
+ 24. **Color inheritance — prefer automatic inheritance, skip inside foreign contexts**:
556
+ - When the parent is a bumper `Typography.*` and the child has no `color` prop, inheritance works automatically via Tamagui's styled context — do not add an explicit `color`.
557
+ - When the ancestor is a kitt-universal component that provides color via its own context — specifically `Button`, `RadioGroup`, or `CustomTooltip` — **do not migrate** the nested `Typography.*`. Leave it on `@ornikar/kitt-universal`. Bumper cannot read those foreign contexts, so any migration would either regress the color or require a guessed explicit token.
558
+ 25. **Icon `align` → `alignSelf`**: Rename `align` to `alignSelf`.
559
+ 26. **Unsupported icon sizes**: If the source `size` is a numeric value other than `16` / `20` / `24`, do NOT migrate. Either leave the call site on `@ornikar/kitt-universal`'s `TypographyIcon`, or flag with a `// TODO: bumper has no matching icon size` comment.
496
560
 
497
561
  ## Not Migratable (requires human review)
498
562
 
@@ -504,3 +568,5 @@ const color = getTypographyColor(state); // should return '$content.accent', '$c
504
568
  - **NativeBase styled() extensions** — Must be migrated to Tamagui `styled()` with updated prop names.
505
569
  - **Type imports** — `TypographyTextProps`, `TypographyHeadingProps`, `TypographyColor`, `ExtendedTypographyColor` type imports must be updated to bumper equivalents.
506
570
  - **Platform-specific overrides merging** — Source has separate `_ios` and `_android` props. Target only has `$platform-native`. If different iOS/Android overrides were used, they need reconciliation.
571
+ - **TypographyIcon with unsupported `size`** — Any numeric `size` other than `16` / `20` / `24` has no bumper token. Call sites must either stay on `@ornikar/kitt-universal` or be resized to a supported token manually.
572
+ - **`Typography.*` nested inside `Button`, `RadioGroup`, or `CustomTooltip`** — These components provide text color via kitt's own (non-Tamagui) context; bumper cannot inherit from them. Leave such call sites on `@ornikar/kitt-universal`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ornikar/bumper",
3
- "version": "3.10.0",
3
+ "version": "3.10.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "directory": "@ornikar/bumper",
@@ -30,7 +30,7 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@babel/runtime": "^7.24.0",
33
- "@ornikar/kitt-icons": "^15.3.0",
33
+ "@ornikar/kitt-icons": "^15.3.1",
34
34
  "@tamagui/core": "1.144.2",
35
35
  "@tamagui/image": "1.144.2",
36
36
  "@tamagui/scroll-view": "1.144.2"
@@ -1,9 +1,6 @@
1
1
  {
2
2
  "root": true,
3
3
  "parser": "@typescript-eslint/parser",
4
- "parserOptions": {
5
- "project": "@ornikar/bumper/tsconfig.eslint.json"
6
- },
7
4
  "extends": [
8
5
  "@ornikar/eslint-config-typescript-react",
9
6
  "@ornikar/eslint-config/rollup",
@@ -12,8 +9,8 @@
12
9
  ],
13
10
  "settings": {
14
11
  "import/resolver": {
15
- "node": {
16
- "moduleDirectory": ["node_modules", "src"]
12
+ "typescript": {
13
+ "project": "@ornikar/bumper/tsconfig.json"
17
14
  }
18
15
  }
19
16
  },
@@ -2,8 +2,14 @@ import { styled } from '@tamagui/core';
2
2
  import type { ReactNode } from 'react';
3
3
  import type { Except } from 'type-fest';
4
4
  import { VStack, type VStackProps } from '../../system/core/primitives/Stack';
5
+ import type { ViewProps } from '../../system/core/primitives/View';
6
+ import type { PropsToTamaguiVariants } from '../../system/types';
5
7
  import { StoryTitle } from './StoryTitle';
6
8
 
9
+ interface InternalStorySectionWithoutMediaProps {
10
+ withBackground?: boolean;
11
+ }
12
+
7
13
  const InternalStorySection = styled(VStack, {
8
14
  paddingVertical: '$space.4',
9
15
  paddingHorizontal: '$space.4',
@@ -12,8 +18,9 @@ const InternalStorySection = styled(VStack, {
12
18
  true: {
13
19
  backgroundColor: '$content.base.hi',
14
20
  },
21
+ false: {},
15
22
  },
16
- } as const,
23
+ } as const satisfies PropsToTamaguiVariants<InternalStorySectionWithoutMediaProps, ViewProps>,
17
24
  });
18
25
 
19
26
  export type StorySectionProps = Except<VStackProps, 'marginBottom'> & {
@@ -1,5 +1,11 @@
1
+ import type { GetProps } from '@tamagui/core';
1
2
  import { styled } from '@tamagui/core';
2
3
  import { InternalTypography } from '../../system/content/typography/Typography';
4
+ import type { PropsToTamaguiVariants } from '../../system/types';
5
+
6
+ interface StoryTitleWithoutMediaProps {
7
+ level?: 1 | 2 | 3;
8
+ }
3
9
 
4
10
  export const StoryTitle = styled(InternalTypography, {
5
11
  variants: {
@@ -8,9 +14,9 @@ export const StoryTitle = styled(InternalTypography, {
8
14
  2: { variant: 'heading-l', marginBottom: '$space.4' },
9
15
  3: { variant: 'heading-m', marginBottom: '$space.4' },
10
16
  },
11
- } as const,
17
+ } as const satisfies PropsToTamaguiVariants<StoryTitleWithoutMediaProps, GetProps<typeof InternalTypography>>,
12
18
 
13
19
  defaultVariants: {
14
20
  level: 1,
15
21
  },
16
- } as const);
22
+ });
@@ -1,14 +1,16 @@
1
1
  import { styled, withStaticProperties } from '@tamagui/core';
2
2
  import { useProps } from '../../core/hooks/useProps';
3
+ import type { HStackProps } from '../../core/primitives/Stack';
3
4
  import { HStack } from '../../core/primitives/Stack';
4
5
  import { View } from '../../core/primitives/View';
5
6
  import { Loader } from '../../loading/loader/Loader';
7
+ import type { PropsToTamaguiVariants } from '../../types';
6
8
  import { ButtonBadge } from './components/ButtonBadge';
7
9
  import { ButtonIcon } from './components/ButtonIcon';
8
10
  import { ButtonText } from './components/ButtonText';
9
11
  import type { ButtonType } from './context';
10
12
  import { context } from './context';
11
- import type { ButtonProps, ButtonTypeStyle } from './types';
13
+ import type { ButtonProps, ButtonTypeStyle, ButtonWithoutMediaProps } from './types';
12
14
 
13
15
  export const InternalButtonFrame = styled(HStack, {
14
16
  name: 'Button',
@@ -125,7 +127,7 @@ export const InternalButtonFrame = styled(HStack, {
125
127
  },
126
128
  false: {},
127
129
  },
128
- } as const,
130
+ } as const satisfies PropsToTamaguiVariants<ButtonWithoutMediaProps, HStackProps>,
129
131
  });
130
132
 
131
133
  export const InternalButton = InternalButtonFrame.styleable<ButtonProps, ButtonProps>((props, ref) => {
@@ -1,7 +1,8 @@
1
1
  import { styled, withStaticProperties } from '@tamagui/core';
2
2
  import type { ReactNode } from 'react';
3
3
  import type { Except } from 'type-fest';
4
- import type { TamaguiMediaProps } from '../../types';
4
+ import type { ViewProps } from '../../core/primitives/View';
5
+ import type { PropsToTamaguiVariants, TamaguiMediaProps } from '../../types';
5
6
  import { InternalButton } from '../Button/Button';
6
7
  import { ButtonBadge } from '../Button/components/ButtonBadge';
7
8
  import { ButtonIcon } from '../Button/components/ButtonIcon';
@@ -31,7 +32,7 @@ export const InternalIconButtonFrame = styled(InternalButton, {
31
32
  height: 48,
32
33
  },
33
34
  },
34
- } as const,
35
+ } as const satisfies PropsToTamaguiVariants<IconButtonWithoutMediaProps, ViewProps>,
35
36
  });
36
37
 
37
38
  function InternalIconButton(props: IconButtonProps): ReactNode {
@@ -3,9 +3,14 @@ import { styled, useStyle } from '@tamagui/core';
3
3
  import type { ReactElement, ReactNode } from 'react';
4
4
  import { cloneElement } from 'react';
5
5
  import { useProps } from '../../core/hooks/useProps';
6
+ import type { ViewProps } from '../../core/primitives/View';
6
7
  import { View } from '../../core/primitives/View';
7
8
  import type { BumperIconTokens } from '../../core/tokens/bumperIcon';
8
- import type { TamaguiMediaProps } from '../../types';
9
+ import type { PropsToTamaguiVariants, TamaguiMediaProps } from '../../types';
10
+
11
+ interface IconContainerVariantProps {
12
+ size?: BumperIconTokens;
13
+ }
9
14
 
10
15
  const IconContainer = styled(View, {
11
16
  name: 'Icon',
@@ -16,7 +21,7 @@ const IconContainer = styled(View, {
16
21
  height: tokens.bumperIcon[iconSize],
17
22
  };
18
23
  },
19
- } as const,
24
+ } as const satisfies PropsToTamaguiVariants<IconContainerVariantProps, ViewProps>,
20
25
 
21
26
  defaultVariants: {
22
27
  size: '$icon.m',
@@ -1,9 +1,9 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react';
2
2
  import { VStack } from '../../core/primitives/Stack';
3
- import type { BodyProps, TypographyTextProps } from './Typography';
3
+ import type { TypographyBodyProps, TypographyTextProps } from './Typography';
4
4
  import { Typography } from '.';
5
5
 
6
- const meta: Meta<Extract<TypographyTextProps, BodyProps>> = {
6
+ const meta: Meta<Extract<TypographyTextProps, TypographyBodyProps>> = {
7
7
  title: 'Bumper/Content/Typography/Features',
8
8
  component: Typography.Text,
9
9
  };
@@ -1,9 +1,9 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react';
2
2
  import { contentColorArgType } from '../../../shared/storybook/helpers/argsHelpers';
3
- import type { BodyProps, TypographyTextProps } from './Typography';
3
+ import type { TypographyBodyProps, TypographyTextProps } from './Typography';
4
4
  import { Typography } from '.';
5
5
 
6
- const meta: Meta<Extract<TypographyTextProps, BodyProps>> = {
6
+ const meta: Meta<Extract<TypographyTextProps, TypographyBodyProps>> = {
7
7
  title: 'Bumper/Content/Typography',
8
8
  component: Typography.Text,
9
9
  argTypes: {
@@ -1,4 +1,4 @@
1
- import type { ColorTokens, GetProps } from '@tamagui/core';
1
+ import type { ColorTokens, TextProps } from '@tamagui/core';
2
2
  import { Text, styled } from '@tamagui/core';
3
3
  import type { ReactNode } from 'react';
4
4
  import { type Except } from 'type-fest';
@@ -13,13 +13,11 @@ import {
13
13
  } from '../../core/tokens/GTStandardFont';
14
14
  import { CONTENT_CAPS_VARIANTS, type ContentCapsVariants } from '../../core/tokens/GTStandardNarrowFont';
15
15
  import type { FontVariants } from '../../core/tokens/fonts';
16
- import type { TamaguiMediaProps } from '../../types';
16
+ import type { PropsToTamaguiVariants, TamaguiMediaProps } from '../../types';
17
17
  import { typographyStyleContext } from './utils/typographyContext';
18
18
 
19
- interface TypographyWithoutMediaProps extends TypographyPropsWithoutFontStyleProps {}
20
-
21
19
  // Remove font-related style props from InternalTypography Props
22
- type TypographyExcludedFontStyleProps =
20
+ type InternalTypographyExcludedFontStyleProps =
23
21
  | 'fontFamily'
24
22
  | 'fontSize'
25
23
  | 'lineHeight'
@@ -28,27 +26,30 @@ type TypographyExcludedFontStyleProps =
28
26
  | 'fontWeight'
29
27
  | 'color';
30
28
 
31
- type TypographyPropsWithoutFontStyleProps = Except<InternalTypographyProps, TypographyExcludedFontStyleProps> & {
29
+ type InternalTypographyPropsWithoutFontStyleProps = Except<TextProps, InternalTypographyExcludedFontStyleProps> & {
32
30
  color?: ColorTokens;
33
31
  };
34
32
 
35
- export interface BodyProps extends TypographyWithoutMediaProps {
33
+ interface InternalTypographyWithoutMediaProps extends InternalTypographyPropsWithoutFontStyleProps {}
34
+
35
+ interface TypographyBodyWithoutMediaProps extends InternalTypographyWithoutMediaProps {
36
36
  variant?: BodyFontVariants;
37
37
  weight?: 'regular' | 'bold';
38
38
  }
39
- export interface HeadingLabelProps extends TypographyWithoutMediaProps {
39
+ interface TypographyHeadingLabelWithoutMediaProps extends InternalTypographyWithoutMediaProps {
40
40
  variant: HeadingFontVariants | LabelFontVariants;
41
41
  weight?: 'semibold';
42
42
  }
43
- export interface ContentCapsProps extends TypographyWithoutMediaProps {
43
+ interface TypographyContentCapsWithoutMediaProps extends InternalTypographyWithoutMediaProps {
44
44
  variant: ContentCapsVariants;
45
45
  weight?: 'bold';
46
46
  }
47
47
 
48
- export type TypographyTextProps =
49
- | TamaguiMediaProps<BodyProps>
50
- | TamaguiMediaProps<HeadingLabelProps>
51
- | TamaguiMediaProps<ContentCapsProps>;
48
+ export interface TypographyBodyProps extends TamaguiMediaProps<TypographyBodyWithoutMediaProps> {}
49
+ export interface TypographyHeadingLabelProps extends TamaguiMediaProps<TypographyHeadingLabelWithoutMediaProps> {}
50
+ export interface TypographyContentCapsProps extends TamaguiMediaProps<TypographyContentCapsWithoutMediaProps> {}
51
+
52
+ export type TypographyTextProps = TypographyBodyProps | TypographyHeadingLabelProps | TypographyContentCapsProps;
52
53
 
53
54
  export const InternalTypography = styled(Text, {
54
55
  context: typographyStyleContext,
@@ -63,9 +64,9 @@ export const InternalTypography = styled(Text, {
63
64
  WebkitFontSmoothing: 'antialiased',
64
65
  },
65
66
  variants: {
66
- variant: (variant: FontVariants) => {
67
+ variant: (variant: FontVariants): TextProps => {
67
68
  const commonVariantStyles = {
68
- fontSize: `$${variant}`,
69
+ fontSize: `$${variant}` as const,
69
70
  lineHeight: `$${variant}`,
70
71
  letterSpacing: `$${variant}`,
71
72
  };
@@ -100,7 +101,7 @@ export const InternalTypography = styled(Text, {
100
101
  };
101
102
  }
102
103
 
103
- return undefined;
104
+ return {};
104
105
  },
105
106
  weight: {
106
107
  regular: {
@@ -113,14 +114,15 @@ export const InternalTypography = styled(Text, {
113
114
  fontWeight: '$bold',
114
115
  },
115
116
  },
116
- } as const,
117
+ } as const satisfies PropsToTamaguiVariants<
118
+ TypographyBodyWithoutMediaProps | TypographyHeadingLabelWithoutMediaProps | TypographyContentCapsWithoutMediaProps,
119
+ TextProps
120
+ >,
117
121
  defaultVariants: {
118
122
  weight: 'regular',
119
123
  },
120
124
  });
121
125
 
122
- export type InternalTypographyProps = GetProps<typeof InternalTypography>;
123
-
124
126
  export function TypographyBase(props: TypographyTextProps): ReactNode {
125
127
  const flattenProps = useProps(props);
126
128
  return <InternalTypography {...flattenProps} />;
@@ -1,12 +1,12 @@
1
1
  import { action } from '@storybook/addon-actions';
2
2
  import type { Meta, StoryObj } from '@storybook/react';
3
3
  import { VStack } from '../../core/primitives/Stack';
4
- import type { BodyProps } from './Typography';
4
+ import type { TypographyBodyProps } from './Typography';
5
5
  import type { TypographyLinkProps } from './TypographyLink';
6
6
  import { InternalTypographyLink, TypographyLink } from './TypographyLink';
7
7
  import { Typography } from '.';
8
8
 
9
- const meta: Meta<Extract<TypographyLinkProps, BodyProps>> = {
9
+ const meta: Meta<Extract<TypographyLinkProps, TypographyBodyProps>> = {
10
10
  title: 'Bumper/Content/TypographyLink/Features',
11
11
  component: TypographyLink,
12
12
  };
@@ -1,11 +1,11 @@
1
1
  import { action } from '@storybook/addon-actions';
2
2
  import type { Meta, StoryObj } from '@storybook/react';
3
3
  import { contentColorArgType } from '../../../shared/storybook/helpers/argsHelpers';
4
- import type { BodyProps } from './Typography';
4
+ import type { TypographyBodyProps } from './Typography';
5
5
  import type { TypographyLinkProps } from './TypographyLink';
6
6
  import { TypographyLink } from './TypographyLink';
7
7
 
8
- const meta: Meta<Extract<TypographyLinkProps, BodyProps>> = {
8
+ const meta: Meta<Extract<TypographyLinkProps, TypographyBodyProps>> = {
9
9
  title: 'Bumper/Content/TypographyLink',
10
10
  component: TypographyLink,
11
11
  argTypes: {
@@ -1,6 +1,7 @@
1
+ import type { TextProps } from '@tamagui/core';
1
2
  import { styled } from '@tamagui/core';
2
3
  import type { ReactNode } from 'react';
3
- import type { TamaguiMediaProps } from '../../types';
4
+ import type { PropsToTamaguiVariants, TamaguiMediaProps } from '../../types';
4
5
  import type { TypographyTextProps } from './Typography';
5
6
  import { InternalTypography } from './Typography';
6
7
  import { typographyStyleContext } from './utils/typographyContext';
@@ -9,6 +10,7 @@ export interface TypographyLinkWithoutMediaProps {
9
10
  disabled?: boolean;
10
11
  noUnderline?: boolean;
11
12
  onPress?: () => void;
13
+ href?: string;
12
14
  }
13
15
 
14
16
  export type TypographyLinkProps = TypographyTextProps & TamaguiMediaProps<TypographyLinkWithoutMediaProps>;
@@ -41,7 +43,7 @@ export const InternalTypographyLink = styled(InternalTypography, {
41
43
  textDecorationLine: 'underline',
42
44
  },
43
45
  },
44
- } as const,
46
+ } as const satisfies PropsToTamaguiVariants<TypographyLinkWithoutMediaProps, TextProps>,
45
47
  defaultVariants: {
46
48
  disabled: false,
47
49
  noUnderline: false,
@@ -2,8 +2,9 @@ import { styled, useStyle } from '@tamagui/core';
2
2
  import type { ReactNode } from 'react';
3
3
  import { useEffect } from 'react';
4
4
  import { useSharedValue, withRepeat, withTiming } from 'react-native-reanimated';
5
+ import type { ViewProps } from '../../core/primitives/View';
5
6
  import { View } from '../../core/primitives/View';
6
- import type { TamaguiMediaProps } from '../../types';
7
+ import type { PropsToTamaguiVariants, TamaguiMediaProps } from '../../types';
7
8
  import { LoaderBackgroundCircle } from './LoaderBackgroundCircle';
8
9
  import { LoaderCircleWrapper } from './LoaderCircleWrapper';
9
10
  import { LoaderForegroundCircle } from './LoaderForegroundCircle';
@@ -12,19 +13,7 @@ import { LOADER_ANIMATION, LOADER_SIZE_CONFIG } from './loaderConfig';
12
13
 
13
14
  const { icon, page } = LOADER_SIZE_CONFIG;
14
15
 
15
- const LoaderContainer = styled(View, {
16
- name: 'Loader',
17
- position: 'relative',
18
- rotate: '-90deg',
19
- variants: {
20
- size: {
21
- icon: { width: icon.size, height: icon.size },
22
- page: { width: page.size, height: page.size },
23
- },
24
- } as const,
25
- });
26
-
27
- interface InternalLoaderProps {
16
+ interface InternalLoaderWithoutMediaProps {
28
17
  /** The size of the loader. `'icon'` renders a small 20px loader, `'page'` renders a larger 48px loader.
29
18
  * @default 'page'
30
19
  * */
@@ -37,7 +26,19 @@ interface InternalLoaderProps {
37
26
  testID?: string;
38
27
  }
39
28
 
40
- export type LoaderProps = TamaguiMediaProps<InternalLoaderProps>;
29
+ const LoaderContainer = styled(View, {
30
+ name: 'Loader',
31
+ position: 'relative',
32
+ rotate: '-90deg',
33
+ variants: {
34
+ size: {
35
+ icon: { width: icon.size, height: icon.size },
36
+ page: { width: page.size, height: page.size },
37
+ },
38
+ } as const satisfies PropsToTamaguiVariants<InternalLoaderWithoutMediaProps, ViewProps>,
39
+ });
40
+
41
+ export type LoaderProps = TamaguiMediaProps<InternalLoaderWithoutMediaProps>;
41
42
 
42
43
  export function Loader({ size = 'page', isOnContrasted = false, testID }: LoaderProps): ReactNode {
43
44
  const backgroundStyle = useStyle({ color: '$border.base.mid' });
@@ -1,9 +1,16 @@
1
1
  import { styled } from '@tamagui/core';
2
+ import type { CircleProps } from 'react-native-svg';
2
3
  import { Circle } from 'react-native-svg';
4
+ import type { PropsToTamaguiVariants } from '../../types';
5
+ import type { LoaderSize } from './loaderConfig';
3
6
  import { LOADER_SIZE_CONFIG } from './loaderConfig';
4
7
 
5
8
  const { icon, page } = LOADER_SIZE_CONFIG;
6
9
 
10
+ interface LoaderBackgroundCircleVariantProps {
11
+ size?: LoaderSize;
12
+ }
13
+
7
14
  export const LoaderBackgroundCircle = styled(Circle, {
8
15
  fill: 'none',
9
16
  variants: {
@@ -11,5 +18,5 @@ export const LoaderBackgroundCircle = styled(Circle, {
11
18
  icon: { strokeWidth: icon.strokeWidth, cx: icon.center, cy: icon.center, r: icon.radius },
12
19
  page: { strokeWidth: page.strokeWidth, cx: page.center, cy: page.center, r: page.radius },
13
20
  },
14
- } as const,
21
+ } as const satisfies PropsToTamaguiVariants<LoaderBackgroundCircleVariantProps, CircleProps>,
15
22
  });
@@ -1,9 +1,18 @@
1
1
  import { styled } from '@tamagui/core';
2
+ import type { SvgProps } from 'react-native-svg';
2
3
  import Svg from 'react-native-svg';
4
+ import type { ViewProps } from '../../core/primitives/View';
5
+ import type { PropsToTamaguiVariants } from '../../types';
6
+ import type { LoaderSize } from './loaderConfig';
3
7
  import { LOADER_SIZE_CONFIG } from './loaderConfig';
4
8
 
5
9
  const { icon, page } = LOADER_SIZE_CONFIG;
6
10
 
11
+ interface LoaderCircleWrapperVariantProps {
12
+ size?: LoaderSize;
13
+ isForeground?: boolean;
14
+ }
15
+
7
16
  export const LoaderCircleWrapper = styled(Svg, {
8
17
  variants: {
9
18
  size: {
@@ -14,6 +23,7 @@ export const LoaderCircleWrapper = styled(Svg, {
14
23
  true: {
15
24
  position: 'absolute',
16
25
  },
26
+ false: {},
17
27
  },
18
- } as const,
28
+ } as const satisfies PropsToTamaguiVariants<LoaderCircleWrapperVariantProps, SvgProps & ViewProps>,
19
29
  });
@@ -1,5 +1,9 @@
1
- // eslint-disable-next-line no-restricted-imports
2
- import type { PropsWithoutMediaStyles as InternalPropsWithoutMediaStyles, WithMediaProps } from '@tamagui/core';
1
+ import type {
2
+ // eslint-disable-next-line no-restricted-imports
3
+ PropsWithoutMediaStyles as InternalPropsWithoutMediaStyles,
4
+ TokensParsed,
5
+ WithMediaProps,
6
+ } from '@tamagui/core';
3
7
 
4
8
  /**
5
9
  * Rewrite of Tamagui PropsWithoutMediaStyles to make all responsive props optionnal.
@@ -17,3 +21,48 @@ export type PropsWithoutMediaStyles<A> = {
17
21
  * pass breakpoint-specific prop objects alongside regular props.
18
22
  */
19
23
  export type TamaguiMediaProps<A> = PropsWithoutMediaStyles<A> & WithMediaProps<InternalPropsWithoutMediaStyles<A>>;
24
+
25
+ /**
26
+ * A list of keys to ignore as variants.
27
+ * If you do not want to ignore keys globally, you should instead use the third param of the {@link PropsToTamaguiVariants}.
28
+ */
29
+ export type DefaultIgnoredKeys = 'children';
30
+
31
+ /**
32
+ * Maps a component's variant-prop interface `T` to a Tamagui `variants` config whose entries produce
33
+ * style objects of type `V`.
34
+ *
35
+ * For each key `K` of `T`, the entry must be either:
36
+ * - a record keyed by the prop's literal values (with `boolean` props normalised to `'true' | 'false'`),
37
+ * where each value is a style `V` or a function returning `V`, or
38
+ * - a function that receives the raw prop value and a {@link VariantGoodiesParams} context and returns
39
+ * a style `V` (or `undefined`).
40
+ *
41
+ * Pass a union of keys as `IgnoredKeys` to skip props that should never be variants (e.g.
42
+ * event handlers). Ignored keys are stripped only from both the top-level map, not from `params.props`.
43
+ *
44
+ * Intended to be used with `satisfies` on the `variants` field of a `styled(...)` call so that the
45
+ * config is inferred `as const` while still being checked against the component's prop types, e.g.
46
+ * `{ ... } as const satisfies PropsToTamaguiVariants<MyVariantProps, ViewProps, 'size'>`.
47
+ */
48
+ export type PropsToTamaguiVariants<T, V, IgnoredKeys extends keyof T = never> = {
49
+ [K in keyof T as K extends IgnoredKeys | Extract<keyof T, DefaultIgnoredKeys> ? never : K]: NonNullable<
50
+ T[K]
51
+ > extends boolean
52
+ ? Record<'true' | 'false', V | ((value: boolean | string | number, params: VariantGoodiesParams<T>) => V)>
53
+ : NonNullable<T[K]> extends PropertyKey
54
+ ? Record<NonNullable<T[K]>, V> | ((value: NonNullable<T[K]>, params: VariantGoodiesParams<T>) => V | undefined)
55
+ : never;
56
+ };
57
+
58
+ /**
59
+ * Second argument passed to function-form variants declared via {@link PropsToTamaguiVariants}.
60
+ *
61
+ * - `props`: the flattened props of the instance being styled, so a variant can branch on sibling
62
+ * props (e.g. read `disabled` or `isOnContrasted` while resolving `type`).
63
+ * - `tokens`: Tamagui's parsed token map, for resolving design tokens from inside the variant.
64
+ */
65
+ interface VariantGoodiesParams<T> {
66
+ props: T;
67
+ tokens: TokensParsed;
68
+ }