@factorialco/f0-react-native 0.29.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/lib/module/components/Avatars/IconAvatar/index.js +1 -1
  2. package/lib/module/components/Avatars/IconAvatar/index.js.map +1 -1
  3. package/lib/module/components/Avatars/ModuleAvatar/index.js +1 -1
  4. package/lib/module/components/Avatars/ModuleAvatar/index.js.map +1 -1
  5. package/lib/module/components/Badge/index.js +1 -1
  6. package/lib/module/components/Badge/index.js.map +1 -1
  7. package/lib/module/components/Button/index.js +1 -1
  8. package/lib/module/components/Button/index.js.map +1 -1
  9. package/lib/module/components/Button/index.spec.js +1 -1
  10. package/lib/module/components/Button/index.spec.js.map +1 -1
  11. package/lib/module/components/Icon/index.js +1 -1
  12. package/lib/module/components/Icon/index.js.map +1 -1
  13. package/lib/module/components/OneChip/index.js +1 -1
  14. package/lib/module/components/OneChip/index.js.map +1 -1
  15. package/lib/module/components/Tags/AlertTab/index.js +1 -1
  16. package/lib/module/components/Tags/AlertTab/index.js.map +1 -1
  17. package/lib/module/components/Tags/RawTag/index.js +1 -1
  18. package/lib/module/components/Tags/RawTag/index.js.map +1 -1
  19. package/lib/module/components/experimental/Lists/DataList/ItemContainer.js +1 -1
  20. package/lib/module/components/experimental/Lists/DataList/ItemContainer.js.map +1 -1
  21. package/lib/module/components/experimental/Lists/DataList/actions/CopyAction.js +1 -1
  22. package/lib/module/components/experimental/Lists/DataList/actions/CopyAction.js.map +1 -1
  23. package/lib/module/components/experimental/Lists/DataList/actions/GenericAction.js +1 -1
  24. package/lib/module/components/experimental/Lists/DataList/actions/GenericAction.js.map +1 -1
  25. package/lib/module/components/exports.js +1 -1
  26. package/lib/module/components/exports.js.map +1 -1
  27. package/lib/module/components/primitives/F0Icon/F0Icon.js +2 -0
  28. package/lib/module/components/primitives/F0Icon/F0Icon.js.map +1 -0
  29. package/lib/module/components/primitives/F0Icon/F0Icon.md +187 -0
  30. package/lib/module/components/primitives/F0Icon/F0Icon.styles.js +2 -0
  31. package/lib/module/components/primitives/F0Icon/F0Icon.styles.js.map +1 -0
  32. package/lib/module/components/primitives/F0Icon/F0Icon.types.js +2 -0
  33. package/lib/module/components/primitives/F0Icon/F0Icon.types.js.map +1 -0
  34. package/lib/module/components/primitives/F0Icon/index.js +2 -0
  35. package/lib/module/components/primitives/F0Icon/index.js.map +1 -0
  36. package/lib/module/icons/index.js +1 -1
  37. package/lib/module/icons/index.js.map +1 -1
  38. package/lib/typescript/components/Activity/ActivityItem/index.d.ts +1 -1
  39. package/lib/typescript/components/Activity/ActivityItem/index.d.ts.map +1 -1
  40. package/lib/typescript/components/Avatars/IconAvatar/index.d.ts +1 -1
  41. package/lib/typescript/components/Avatars/IconAvatar/index.d.ts.map +1 -1
  42. package/lib/typescript/components/Avatars/ModuleAvatar/index.d.ts +1 -1
  43. package/lib/typescript/components/Avatars/ModuleAvatar/index.d.ts.map +1 -1
  44. package/lib/typescript/components/Badge/index.d.ts +1 -1
  45. package/lib/typescript/components/Badge/index.d.ts.map +1 -1
  46. package/lib/typescript/components/Button/index.d.ts +1 -1
  47. package/lib/typescript/components/Button/index.d.ts.map +1 -1
  48. package/lib/typescript/components/Icon/index.d.ts +7 -14
  49. package/lib/typescript/components/Icon/index.d.ts.map +1 -1
  50. package/lib/typescript/components/OneChip/index.d.ts +1 -1
  51. package/lib/typescript/components/OneChip/index.d.ts.map +1 -1
  52. package/lib/typescript/components/Tags/RawTag/index.d.ts +1 -1
  53. package/lib/typescript/components/Tags/RawTag/index.d.ts.map +1 -1
  54. package/lib/typescript/components/experimental/Lists/DataList/ItemContainer.d.ts +1 -1
  55. package/lib/typescript/components/experimental/Lists/DataList/ItemContainer.d.ts.map +1 -1
  56. package/lib/typescript/components/experimental/Lists/DataList/actions/CopyAction.d.ts.map +1 -1
  57. package/lib/typescript/components/experimental/Lists/DataList/index.d.ts +1 -1
  58. package/lib/typescript/components/experimental/Lists/DataList/index.d.ts.map +1 -1
  59. package/lib/typescript/components/exports.d.ts +2 -1
  60. package/lib/typescript/components/exports.d.ts.map +1 -1
  61. package/lib/typescript/components/primitives/F0Icon/F0Icon.d.ts +25 -0
  62. package/lib/typescript/components/primitives/F0Icon/F0Icon.d.ts.map +1 -0
  63. package/lib/typescript/components/primitives/F0Icon/F0Icon.styles.d.ts +90 -0
  64. package/lib/typescript/components/primitives/F0Icon/F0Icon.styles.d.ts.map +1 -0
  65. package/lib/typescript/components/primitives/F0Icon/F0Icon.types.d.ts +47 -0
  66. package/lib/typescript/components/primitives/F0Icon/F0Icon.types.d.ts.map +1 -0
  67. package/lib/typescript/components/primitives/F0Icon/index.d.ts +10 -0
  68. package/lib/typescript/components/primitives/F0Icon/index.d.ts.map +1 -0
  69. package/lib/typescript/icons/index.d.ts +0 -1
  70. package/lib/typescript/icons/index.d.ts.map +1 -1
  71. package/package.json +1 -1
  72. package/src/components/Activity/ActivityItem/index.spec.tsx +1 -1
  73. package/src/components/Activity/ActivityItem/index.tsx +1 -1
  74. package/src/components/Avatars/IconAvatar/index.tsx +6 -2
  75. package/src/components/Avatars/ModuleAvatar/index.tsx +1 -1
  76. package/src/components/Badge/index.tsx +2 -2
  77. package/src/components/Button/index.spec.tsx +3 -4
  78. package/src/components/Button/index.tsx +19 -13
  79. package/src/components/Icon/__tests__/Icon.spec.tsx +0 -4
  80. package/src/components/Icon/index.tsx +7 -26
  81. package/src/components/OneChip/index.tsx +3 -3
  82. package/src/components/Tags/AlertTab/index.tsx +2 -2
  83. package/src/components/Tags/RawTag/index.tsx +2 -2
  84. package/src/components/experimental/Lists/DataList/ItemContainer.tsx +2 -2
  85. package/src/components/experimental/Lists/DataList/actions/CopyAction.tsx +7 -10
  86. package/src/components/experimental/Lists/DataList/actions/GenericAction.tsx +2 -2
  87. package/src/components/experimental/Lists/DataList/index.tsx +1 -1
  88. package/src/components/experimental/Lists/DetailsItem/__snapshots__/index.spec.tsx.snap +4 -4
  89. package/src/components/experimental/Lists/DetailsItemsList/__snapshots__/index.spec.tsx.snap +1 -1
  90. package/src/components/exports.ts +2 -1
  91. package/src/components/primitives/F0Icon/F0Icon.md +187 -0
  92. package/src/components/primitives/F0Icon/F0Icon.styles.ts +43 -0
  93. package/src/components/primitives/F0Icon/F0Icon.tsx +73 -0
  94. package/src/components/primitives/F0Icon/F0Icon.types.ts +77 -0
  95. package/src/components/primitives/F0Icon/__tests__/F0Icon.spec.tsx +131 -0
  96. package/src/components/primitives/F0Icon/__tests__/F0Icon.tokens.spec.ts +39 -0
  97. package/src/components/primitives/F0Icon/index.ts +10 -0
  98. package/src/icons/index.ts +0 -1
  99. package/lib/module/components/Icon/README.md +0 -63
  100. package/src/components/Icon/README.md +0 -63
@@ -3,7 +3,7 @@ import { Pressable, Text, View } from "react-native"
3
3
  import { tv, type VariantProps } from "tailwind-variants"
4
4
 
5
5
  import { cn } from "../../lib/utils"
6
- import { Icon, type IconType } from "../Icon"
6
+ import { F0Icon, type IconType, type IconColor } from "../primitives/F0Icon"
7
7
 
8
8
  export const variants = [
9
9
  "default",
@@ -68,29 +68,35 @@ const pressedVariants = tv({
68
68
  },
69
69
  })
70
70
 
71
- const getIconColor = (variant: ButtonVariant, isPressed: boolean) => {
71
+ const getIconColor = (
72
+ variant: ButtonVariant,
73
+ isPressed: boolean
74
+ ): IconColor => {
72
75
  switch (variant) {
73
76
  case "default":
74
- return "text-f0-icon-inverse"
77
+ return "inverse"
75
78
  case "critical":
76
- return isPressed ? "text-f0-icon-inverse" : "text-f0-icon-critical-bold"
79
+ return isPressed ? "inverse" : "critical-bold"
77
80
  default:
78
- return "text-f0-icon"
81
+ return "default"
79
82
  }
80
83
  }
81
84
 
82
- const getIconOnlyColor = (variant: ButtonVariant, isPressed: boolean) => {
85
+ const getIconOnlyColor = (
86
+ variant: ButtonVariant,
87
+ isPressed: boolean
88
+ ): IconColor => {
83
89
  switch (variant) {
84
90
  case "critical":
85
- return isPressed ? "text-f0-icon-inverse" : "text-f0-icon-critical-bold"
91
+ return isPressed ? "inverse" : "critical-bold"
86
92
  case "default":
87
- return "text-f0-icon-inverse"
93
+ return "inverse"
88
94
  case "outline":
89
95
  case "neutral":
90
96
  case "ghost":
91
97
  case "promote":
92
98
  default:
93
- return "text-f0-icon-bold"
99
+ return "bold"
94
100
  }
95
101
  }
96
102
 
@@ -191,15 +197,15 @@ export const Button = forwardRef<View, ButtonProps>(function Button(
191
197
  accessibilityHint={accessibilityHint}
192
198
  >
193
199
  {icon && (
194
- <Icon
200
+ <F0Icon
195
201
  icon={icon}
196
202
  size={size === "sm" ? "sm" : "md"}
197
- className={cn(
198
- hideLabel && round ? undefined : "-ml-0.5",
203
+ color={
199
204
  hideLabel && round
200
205
  ? getIconOnlyColor(variant, shouldShowPressed)
201
206
  : getIconColor(variant, shouldShowPressed)
202
- )}
207
+ }
208
+ className={hideLabel && round ? undefined : "-ml-0.5"}
203
209
  />
204
210
  )}
205
211
  {emoji && (
@@ -20,8 +20,6 @@ describe("Icon", () => {
20
20
  const { getByTestId } = render(
21
21
  <Icon icon={Archive} size="lg" testID="icon" />
22
22
  )
23
- // In a real test environment, we could check the actual style props applied
24
- // but for now we just ensure it renders
25
23
  expect(getByTestId("icon")).toBeTruthy()
26
24
  })
27
25
 
@@ -29,8 +27,6 @@ describe("Icon", () => {
29
27
  const { getByTestId } = render(
30
28
  <Icon icon={Archive} className="text-red-500" testID="icon" />
31
29
  )
32
- // In a real test environment, we could check the actual style props applied
33
- // but for now we just ensure it renders
34
30
  expect(getByTestId("icon")).toBeTruthy()
35
31
  })
36
32
  })
@@ -1,13 +1,9 @@
1
- import React, {
2
- forwardRef,
3
- ForwardRefExoticComponent,
4
- RefAttributes,
5
- } from "react"
1
+ import React, { forwardRef } from "react"
6
2
  import { Svg, SvgProps } from "react-native-svg"
7
3
  import { tv, type VariantProps } from "tailwind-variants"
8
- import { withUniwind } from "uniwind"
9
4
 
10
5
  import { cn } from "../../lib/utils"
6
+ import { applyIconInterop, type IconType } from "../primitives/F0Icon"
11
7
 
12
8
  const iconVariants = tv({
13
9
  base: "shrink-0",
@@ -33,26 +29,11 @@ export interface IconProps extends SvgProps, VariantProps<typeof iconVariants> {
33
29
  isPressed?: boolean
34
30
  }
35
31
 
36
- export type IconType = ForwardRefExoticComponent<
37
- SvgProps &
38
- RefAttributes<Svg> & {
39
- className?: string
40
- }
41
- >
42
-
43
- // Keep track of icons that have already had withUniwind applied
44
- const interopAppliedIcons = new WeakSet()
45
-
46
- // Function to apply UniWind interop to an icon component
47
- export function applyIconInterop(icon: IconType) {
48
- if (!interopAppliedIcons.has(icon)) {
49
- const wrappedIcon = withUniwind(icon)
50
- interopAppliedIcons.add(wrappedIcon)
51
- return wrappedIcon
52
- }
53
- return icon
54
- }
55
-
32
+ /**
33
+ * @deprecated Use F0Icon instead. This component will be removed in a future version.
34
+ * Migration: Replace <Icon icon={X} /> with <F0Icon icon={X} />.
35
+ * F0Icon supports the same `icon`, `size`, `testID`, and `className` props, plus semantic `color` variants.
36
+ */
56
37
  export const Icon = forwardRef<Svg, IconProps>(function Icon(
57
38
  { size = "md", icon, className, testID, ...props },
58
39
  ref
@@ -3,8 +3,8 @@ import { tv, type VariantProps } from "tailwind-variants"
3
3
 
4
4
  import { CrossedCircle } from "../../icons/app"
5
5
  import { cn } from "../../lib/utils"
6
- import { Icon, type IconType } from "../Icon"
7
6
  import { PressableFeedback } from "../PressableFeedback"
7
+ import { F0Icon, type IconType } from "../primitives/F0Icon"
8
8
 
9
9
  export const chipContainerVariants = tv({
10
10
  base: "flex items-center gap-1 rounded-full border border-solid border-f0-border px-2 py-0.5 grow-0",
@@ -61,7 +61,7 @@ export const OneChip = ({
61
61
  >
62
62
  <View className="flex flex-row items-center gap-0.5">
63
63
  {icon && (
64
- <Icon
64
+ <F0Icon
65
65
  icon={icon}
66
66
  size="sm"
67
67
  className={chipTextVariants({ variant })}
@@ -78,7 +78,7 @@ export const OneChip = ({
78
78
  accessibilityRole="button"
79
79
  accessibilityLabel="Close"
80
80
  >
81
- <Icon
81
+ <F0Icon
82
82
  icon={CrossedCircle}
83
83
  className={chipTextVariants({ variant })}
84
84
  size="sm"
@@ -1,7 +1,7 @@
1
1
  import { AlertCircle, InfoCircle, Warning } from "../../../icons/app"
2
2
  import { useTextFormatEnforcer } from "../../../lib/text"
3
3
  import { cn } from "../../../lib/utils"
4
- import { Icon, IconType } from "../../Icon"
4
+ import { F0Icon, type IconType } from "../../primitives/F0Icon"
5
5
  import { BaseTag } from "../BaseTag"
6
6
 
7
7
  type Level = "info" | "warning" | "critical"
@@ -40,7 +40,7 @@ export const AlertTag = <T extends string>({ text, level }: Props<T>) => {
40
40
  }[level]
41
41
  )}
42
42
  left={
43
- <Icon
43
+ <F0Icon
44
44
  icon={iconMap[level]}
45
45
  className={cn(
46
46
  {
@@ -1,6 +1,6 @@
1
1
  import { useTextFormatEnforcer } from "../../../lib/text"
2
2
  import { cn } from "../../../lib/utils"
3
- import { Icon, IconType } from "../../Icon"
3
+ import { F0Icon, type IconType } from "../../primitives/F0Icon"
4
4
  import { BaseTag } from "../BaseTag"
5
5
 
6
6
  export type RawTagProps = {
@@ -29,7 +29,7 @@ export const RawTag = ({
29
29
  classNameText="text-f0-foreground"
30
30
  left={
31
31
  icon ? (
32
- <Icon icon={icon} size="sm" className="text-f0-icon" aria-hidden />
32
+ <F0Icon icon={icon} size="sm" color="default" aria-hidden />
33
33
  ) : null
34
34
  }
35
35
  text={text}
@@ -2,7 +2,7 @@ import { ReactElement, ReactNode } from "react"
2
2
  import { View, Text } from "react-native"
3
3
 
4
4
  import { cn } from "../../../../lib/utils"
5
- import { Icon, IconType } from "../../../Icon"
5
+ import { F0Icon, type IconType } from "../../../primitives/F0Icon"
6
6
 
7
7
  import { CopyAction } from "./actions/CopyAction"
8
8
  import { GenericAction } from "./actions/GenericAction"
@@ -34,7 +34,7 @@ export const ItemContainer = (props: ItemContainerProps) => {
34
34
  (typeof LeftIcon === "function" ? (
35
35
  LeftIcon({})
36
36
  ) : (
37
- <Icon icon={LeftIcon} size="md" />
37
+ <F0Icon icon={LeftIcon} size="md" />
38
38
  ))}
39
39
  <Text className="line-clamp-5 text-left text-f0-foreground">
40
40
  {text}
@@ -5,7 +5,7 @@ import { Pressable, View } from "react-native"
5
5
  import { CopyActionType } from ".."
6
6
  import { CheckCircle, LayersFront } from "../../../../../icons/app"
7
7
  import { cn } from "../../../../../lib/utils"
8
- import { Icon } from "../../../../Icon"
8
+ import { F0Icon } from "../../../../primitives/F0Icon"
9
9
 
10
10
  const COPIED_SHOWN_MS = 750
11
11
 
@@ -47,24 +47,21 @@ export const CopyAction = ({ text, children }: CopyActionProps) => {
47
47
  <View className="flex flex-row items-center gap-1.5">{children}</View>
48
48
  <View className="flex">
49
49
  {!copied && (
50
- <Icon
50
+ <F0Icon
51
51
  icon={LayersFront}
52
52
  size="md"
53
53
  aria-hidden={true}
54
- className={cn(
55
- "col-start-1 col-end-2 row-start-1 row-end-2 text-f0-icon-bold"
56
- )}
54
+ color="bold"
55
+ className="col-start-1 col-end-2 row-start-1 row-end-2"
57
56
  />
58
57
  )}
59
58
  {copied && (
60
- <Icon
59
+ <F0Icon
61
60
  icon={CheckCircle}
62
61
  size="md"
63
62
  aria-hidden={true}
64
- className={cn(
65
- "col-start-1 col-end-2 row-start-1 row-end-2",
66
- "text-f0-icon-positive"
67
- )}
63
+ color="positive"
64
+ className="col-start-1 col-end-2 row-start-1 row-end-2"
68
65
  />
69
66
  )}
70
67
  </View>
@@ -4,7 +4,7 @@ import { Pressable, View } from "react-native"
4
4
  import { GenericActionType } from ".."
5
5
  import { ChevronRight } from "../../../../../icons/app"
6
6
  import { cn } from "../../../../../lib/utils"
7
- import { Icon } from "../../../../Icon"
7
+ import { F0Icon } from "../../../../primitives/F0Icon"
8
8
 
9
9
  export type GenericActionProps = {
10
10
  children: ReactNode
@@ -24,7 +24,7 @@ export const GenericAction = memo(
24
24
  )}
25
25
  >
26
26
  <View className="flex flex-row items-center gap-1.5">{children}</View>
27
- <Icon
27
+ <F0Icon
28
28
  aria-hidden={true}
29
29
  icon={ChevronRight}
30
30
  size="md"
@@ -5,7 +5,7 @@ import { cn } from "../../../../lib/utils"
5
5
  import { CompanyAvatar } from "../../../Avatars/CompanyAvatar"
6
6
  import { PersonAvatar } from "../../../Avatars/PersonAvatar"
7
7
  import { TeamAvatar } from "../../../Avatars/TeamAvatar"
8
- import { IconType } from "../../../Icon"
8
+ import { type IconType } from "../../../primitives/F0Icon"
9
9
  import { DotTag, DotTagProps } from "../../../Tags/DotTag"
10
10
 
11
11
  import { ItemContainer } from "./ItemContainer"
@@ -101,7 +101,7 @@ exports[`DetailsItem Snapshot type company 1`] = `
101
101
  aria-hidden={true}
102
102
  bbHeight="100%"
103
103
  bbWidth="100%"
104
- className="shrink-0 w-5 h-5 stroke-md col-start-1 col-end-2 row-start-1 row-end-2 text-f0-icon-bold"
104
+ className="shrink-0 w-5 h-5 stroke-md text-f0-icon-bold col-start-1 col-end-2 row-start-1 row-end-2"
105
105
  fill="none"
106
106
  focusable={false}
107
107
  meetOrSlice={0}
@@ -343,7 +343,7 @@ exports[`DetailsItem Snapshot type item 1`] = `
343
343
  aria-hidden={true}
344
344
  bbHeight="100%"
345
345
  bbWidth="100%"
346
- className="shrink-0 w-5 h-5 stroke-md col-start-1 col-end-2 row-start-1 row-end-2 text-f0-icon-bold"
346
+ className="shrink-0 w-5 h-5 stroke-md text-f0-icon-bold col-start-1 col-end-2 row-start-1 row-end-2"
347
347
  fill="none"
348
348
  focusable={false}
349
349
  meetOrSlice={0}
@@ -519,7 +519,7 @@ exports[`DetailsItem Snapshot type person 1`] = `
519
519
  aria-hidden={true}
520
520
  bbHeight="100%"
521
521
  bbWidth="100%"
522
- className="shrink-0 w-5 h-5 stroke-md col-start-1 col-end-2 row-start-1 row-end-2 text-f0-icon-bold"
522
+ className="shrink-0 w-5 h-5 stroke-md text-f0-icon-bold col-start-1 col-end-2 row-start-1 row-end-2"
523
523
  fill="none"
524
524
  focusable={false}
525
525
  meetOrSlice={0}
@@ -695,7 +695,7 @@ exports[`DetailsItem Snapshot type team 1`] = `
695
695
  aria-hidden={true}
696
696
  bbHeight="100%"
697
697
  bbWidth="100%"
698
- className="shrink-0 w-5 h-5 stroke-md col-start-1 col-end-2 row-start-1 row-end-2 text-f0-icon-bold"
698
+ className="shrink-0 w-5 h-5 stroke-md text-f0-icon-bold col-start-1 col-end-2 row-start-1 row-end-2"
699
699
  fill="none"
700
700
  focusable={false}
701
701
  meetOrSlice={0}
@@ -79,7 +79,7 @@ exports[`DetailsItemsList Snapshot 1`] = `
79
79
  aria-hidden={true}
80
80
  bbHeight="100%"
81
81
  bbWidth="100%"
82
- className="shrink-0 w-5 h-5 stroke-md col-start-1 col-end-2 row-start-1 row-end-2 text-f0-icon-bold"
82
+ className="shrink-0 w-5 h-5 stroke-md text-f0-icon-bold col-start-1 col-end-2 row-start-1 row-end-2"
83
83
  fill="none"
84
84
  focusable={false}
85
85
  meetOrSlice={0}
@@ -5,7 +5,7 @@ export * from "./Badge"
5
5
  export * from "./Button"
6
6
  export * from "./Counter"
7
7
  export * from "./ExampleComponent"
8
- export * from "./Icon"
8
+ export { Icon, type IconProps } from "./Icon"
9
9
  export * from "./Navigation/PageHeader"
10
10
  export * from "./OneChip"
11
11
  export * from "./OnePreset"
@@ -17,3 +17,4 @@ export * from "./experimental/Lists/DetailsItemsList"
17
17
 
18
18
  // Export primitives
19
19
  export * from "./primitives/F0Text"
20
+ export * from "./primitives/F0Icon"
@@ -0,0 +1,187 @@
1
+ # F0Icon
2
+
3
+ Icon component for rendering SVG icons with consistent sizing and semantic colors from the F0 Design System.
4
+
5
+ ## Overview
6
+
7
+ F0Icon is an atomic component that wraps SVG icon components with automatic UniWind interop, standardized sizing variants, and semantic icon colors. It follows the same pattern as F0Text: a `color` prop maps to design system tokens, with `className` available as an escape hatch.
8
+
9
+ ## Architecture
10
+
11
+ - **Pattern:** Props API (Atomic Component)
12
+ - **Category:** Primitive Component
13
+ - **Location:** `src/components/primitives/F0Icon/`
14
+
15
+ ## Usage
16
+
17
+ <!-- prettier-ignore -->
18
+ ```tsx
19
+ import { F0Icon } from "@factorialco/f0-react-native"
20
+ import { Archive } from "@factorialco/f0-react-native/icons/app"
21
+ import { Home } from "@factorialco/f0-react-native/icons/modules"
22
+
23
+ <F0Icon icon={Archive} />
24
+
25
+ <F0Icon icon={Home} size="lg" />
26
+
27
+ <F0Icon icon={Archive} color="critical" />
28
+
29
+ <F0Icon icon={Archive} size="sm" color="positive" testID="archive-icon" />
30
+ ```
31
+
32
+ ## Props
33
+
34
+ ### `icon` (required)
35
+
36
+ - **Type:** `IconType`
37
+ - **Description:** SVG icon component to render (from `icons/` directory)
38
+
39
+ <!-- prettier-ignore -->
40
+ ```tsx
41
+ import { Archive } from "@factorialco/f0-react-native/icons/app"
42
+ <F0Icon icon={Archive} />
43
+ ```
44
+
45
+ ### `size`
46
+
47
+ - **Type:** `'xl' | 'lg' | 'md' | 'sm' | 'xs'`
48
+ - **Default:** `'md'`
49
+ - **Description:** Size variant for the icon
50
+
51
+ <!-- prettier-ignore -->
52
+ ```tsx
53
+ <F0Icon icon={Archive} size="xl" /> // 32x32px
54
+ <F0Icon icon={Archive} size="lg" /> // 24x24px
55
+ <F0Icon icon={Archive} size="md" /> // 20x20px (default)
56
+ <F0Icon icon={Archive} size="sm" /> // 16x16px
57
+ <F0Icon icon={Archive} size="xs" /> // 12x12px
58
+ ```
59
+
60
+ ### `color`
61
+
62
+ - **Type:** `IconColor` (see below)
63
+ - **Default:** none (no color class applied; icon inherits from parent)
64
+ - **Description:** Semantic icon color from the F0 design system. Maps to `f0-icon-*` tokens.
65
+
66
+ <!-- prettier-ignore -->
67
+ ```tsx
68
+ <F0Icon icon={Archive} color="default" />
69
+ <F0Icon icon={Archive} color="critical" />
70
+ <F0Icon icon={Archive} color="positive" />
71
+ <F0Icon icon={Archive} color="info" />
72
+ <F0Icon icon={Archive} color="warning" />
73
+ <F0Icon icon={Archive} color="accent" />
74
+ ```
75
+
76
+ ### `testID`
77
+
78
+ - **Type:** `string`
79
+ - **Description:** Test identifier for testing library
80
+
81
+ <!-- prettier-ignore -->
82
+ ```tsx
83
+ <F0Icon icon={Archive} testID="my-icon" />
84
+ ```
85
+
86
+ ### `className`
87
+
88
+ - **Type:** `string`
89
+ - **Description:** Tailwind classes for custom styling. Use as an escape hatch when `color` doesn't cover the need (e.g. layout adjustments, one-off colors).
90
+
91
+ <!-- prettier-ignore -->
92
+ ```tsx
93
+ <F0Icon icon={Archive} className="-ml-0.5" />
94
+ <F0Icon icon={Star} className="text-yellow-500" />
95
+ ```
96
+
97
+ ### Other Props
98
+
99
+ F0Icon accepts all SVG props from `react-native-svg` except `style` (use `color` or `className` instead).
100
+
101
+ ## Size Variants
102
+
103
+ | Variant | Width/Height | Stroke Width | Use Case |
104
+ | ------- | -------------- | ------------ | ---------------------------------- |
105
+ | `xl` | 32px (w-8 h-8) | stroke-xl | Large feature icons, hero sections |
106
+ | `lg` | 24px (w-6 h-6) | stroke-lg | Primary buttons, major actions |
107
+ | `md` | 20px (w-5 h-5) | stroke-md | Default size, most UI elements |
108
+ | `sm` | 16px (w-4 h-4) | stroke-sm | Secondary buttons, compact UI |
109
+ | `xs` | 12px (w-3 h-3) | stroke-xs | Inline icons, dense layouts |
110
+
111
+ ## Color Variants
112
+
113
+ Derived from `f0-icon-*` tokens in `src/styles/theme.css`. Sync is enforced by `F0Icon.tokens.spec.ts`.
114
+
115
+ | Color | Token | Use Case |
116
+ | --------------------- | ---------------------------------- | ------------------------- |
117
+ | `default` | `text-f0-icon` | Standard icon color |
118
+ | `secondary` | `text-f0-icon-secondary` | Muted/secondary icons |
119
+ | `inverse` | `text-f0-icon-inverse` | Icons on dark backgrounds |
120
+ | `bold` | `text-f0-icon-bold` | High emphasis icons |
121
+ | `critical` | `text-f0-icon-critical` | Error/destructive |
122
+ | `critical-bold` | `text-f0-icon-critical-bold` | Bold critical emphasis |
123
+ | `accent` | `text-f0-icon-accent` | Brand accent |
124
+ | `info` | `text-f0-icon-info` | Informational |
125
+ | `warning` | `text-f0-icon-warning` | Warning states |
126
+ | `positive` | `text-f0-icon-positive` | Success/positive |
127
+ | `promote` | `text-f0-icon-promote` | Promotional highlights |
128
+ | `selected` | `text-f0-icon-selected` | Selected state |
129
+ | `selected-hover` | `text-f0-icon-selected-hover` | Selected hover state |
130
+ | `mood-super-negative` | `text-f0-icon-mood-super-negative` | Mood: very negative |
131
+ | `mood-negative` | `text-f0-icon-mood-negative` | Mood: negative |
132
+ | `mood-neutral` | `text-f0-icon-mood-neutral` | Mood: neutral |
133
+ | `mood-positive` | `text-f0-icon-mood-positive` | Mood: positive |
134
+ | `mood-super-positive` | `text-f0-icon-mood-super-positive` | Mood: very positive |
135
+
136
+ ## Implementation Details
137
+
138
+ ### UniWind Interop
139
+
140
+ F0Icon automatically applies UniWind interop to icon components using `withUniwind()`. Icons are cached in a `WeakSet` to ensure the wrapper is only applied once per icon type.
141
+
142
+ ### Performance
143
+
144
+ - Wrapped in `React.memo` to prevent unnecessary re-renders
145
+ - `useMemo` for className generation
146
+ - Icon interop caching via `WeakSet`
147
+
148
+ ### TypeScript
149
+
150
+ Style prop is blocked at compile-time to enforce `color`/`className` usage:
151
+
152
+ <!-- prettier-ignore -->
153
+ ```tsx
154
+ <F0Icon icon={Archive} color="critical" />
155
+
156
+ // TypeScript error - style not allowed
157
+ <F0Icon icon={Archive} style={{ color: 'red' }} />
158
+ ```
159
+
160
+ ### Token Sync
161
+
162
+ `ICON_COLORS` in `F0Icon.types.ts` is kept in sync with `f0-icon-*` tokens from `theme.css` by a CI test (`F0Icon.tokens.spec.ts`). Adding or removing a token in theme.css without updating `ICON_COLORS` will fail the test.
163
+
164
+ ## File Structure
165
+
166
+ ```
167
+ src/components/primitives/F0Icon/
168
+ ├── index.ts # Barrel exports
169
+ ├── F0Icon.tsx # Component implementation
170
+ ├── F0Icon.types.ts # TypeScript type definitions (ICON_COLORS, IconColor)
171
+ ├── F0Icon.styles.ts # tailwind-variants configuration (size + color)
172
+ ├── F0Icon.md # This documentation
173
+ └── __tests__/
174
+ ├── F0Icon.spec.tsx # Component tests
175
+ └── F0Icon.tokens.spec.ts # Token sync test (theme.css ↔ ICON_COLORS)
176
+ ```
177
+
178
+ ## Related Components
179
+
180
+ - **F0Text** - Text primitive with semantic `color` prop (foreground tokens)
181
+ - **Button** - Uses F0Icon for icon slots
182
+ - **Badge** - Can include F0Icon
183
+
184
+ ## References
185
+
186
+ - Design tokens: `src/styles/theme.css`
187
+ - Icon source: `src/icons/`
@@ -0,0 +1,43 @@
1
+ import { tv } from "tailwind-variants"
2
+
3
+ /**
4
+ * F0Icon tailwind-variants configuration
5
+ * Size and color variants from the F0 design system
6
+ */
7
+ export const iconVariants = tv({
8
+ base: "shrink-0",
9
+
10
+ variants: {
11
+ size: {
12
+ xl: "w-8 h-8 stroke-xl",
13
+ lg: "w-6 h-6 stroke-lg",
14
+ md: "w-5 h-5 stroke-md",
15
+ sm: "w-4 h-4 stroke-sm",
16
+ xs: "w-3 h-3 stroke-xs",
17
+ },
18
+ color: {
19
+ default: "text-f0-icon",
20
+ secondary: "text-f0-icon-secondary",
21
+ inverse: "text-f0-icon-inverse",
22
+ bold: "text-f0-icon-bold",
23
+ critical: "text-f0-icon-critical",
24
+ "critical-bold": "text-f0-icon-critical-bold",
25
+ accent: "text-f0-icon-accent",
26
+ info: "text-f0-icon-info",
27
+ warning: "text-f0-icon-warning",
28
+ positive: "text-f0-icon-positive",
29
+ promote: "text-f0-icon-promote",
30
+ selected: "text-f0-icon-selected",
31
+ "selected-hover": "text-f0-icon-selected-hover",
32
+ "mood-super-negative": "text-f0-icon-mood-super-negative",
33
+ "mood-negative": "text-f0-icon-mood-negative",
34
+ "mood-neutral": "text-f0-icon-mood-neutral",
35
+ "mood-positive": "text-f0-icon-mood-positive",
36
+ "mood-super-positive": "text-f0-icon-mood-super-positive",
37
+ },
38
+ },
39
+
40
+ defaultVariants: {
41
+ size: "md",
42
+ },
43
+ })
@@ -0,0 +1,73 @@
1
+ import React, { useMemo } from "react"
2
+ import type { Svg } from "react-native-svg"
3
+ import { withUniwind } from "uniwind"
4
+
5
+ import { cn } from "../../../lib/utils"
6
+
7
+ import { iconVariants } from "./F0Icon.styles"
8
+ import type { F0IconProps, IconType } from "./F0Icon.types"
9
+
10
+ // Cache original icon -> wrapped icon so withUniwind is only applied once per icon type
11
+ const interopCache = new WeakMap<IconType, IconType>()
12
+
13
+ /**
14
+ * Applies UniWind interop to an icon component
15
+ * Ensures withUniwind is only applied once per icon type
16
+ * @internal
17
+ */
18
+ export function applyIconInterop(icon: IconType): IconType {
19
+ let wrapped = interopCache.get(icon)
20
+ if (!wrapped) {
21
+ wrapped = withUniwind(icon) as IconType
22
+ interopCache.set(icon, wrapped)
23
+ }
24
+ return wrapped
25
+ }
26
+
27
+ /**
28
+ * F0Icon - Icon component for the F0 Design System
29
+ *
30
+ * Renders SVG icons with consistent sizing and semantic colors.
31
+ * Icons are automatically wrapped with UniWind for className support.
32
+ *
33
+ * @example
34
+ * import { Archive } from '@/icons/app';
35
+ *
36
+ * <F0Icon icon={Archive} size="lg" />
37
+ * <F0Icon icon={Archive} color="critical" />
38
+ * <F0Icon icon={Archive} size="sm" color="positive" />
39
+ */
40
+ const F0Icon = React.memo(
41
+ React.forwardRef<Svg, F0IconProps>(
42
+ (
43
+ { size = "md", color, icon, testID, className: customClassName, ...rest },
44
+ ref
45
+ ) => {
46
+ const IconComponent = useMemo(
47
+ () => (icon ? applyIconInterop(icon) : null),
48
+ [icon]
49
+ )
50
+
51
+ const className = useMemo(
52
+ () => cn(iconVariants({ size, color }), customClassName),
53
+ [size, color, customClassName]
54
+ )
55
+
56
+ // Early return if no icon provided (after all hooks)
57
+ if (!icon || !IconComponent) return null
58
+
59
+ return (
60
+ <IconComponent
61
+ ref={ref}
62
+ className={className}
63
+ testID={testID}
64
+ {...rest}
65
+ />
66
+ )
67
+ }
68
+ )
69
+ )
70
+
71
+ F0Icon.displayName = "F0Icon"
72
+
73
+ export default F0Icon