@factorialco/f0-react-native 0.29.0 → 0.31.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 (130) 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/OnePreset/index.js +1 -1
  16. package/lib/module/components/OnePreset/index.js.map +1 -1
  17. package/lib/module/components/Tags/AlertTab/index.js +1 -1
  18. package/lib/module/components/Tags/AlertTab/index.js.map +1 -1
  19. package/lib/module/components/Tags/BaseTag/index.js +1 -1
  20. package/lib/module/components/Tags/BaseTag/index.js.map +1 -1
  21. package/lib/module/components/Tags/RawTag/index.js +1 -1
  22. package/lib/module/components/Tags/RawTag/index.js.map +1 -1
  23. package/lib/module/components/experimental/Lists/DataList/ItemContainer.js +1 -1
  24. package/lib/module/components/experimental/Lists/DataList/ItemContainer.js.map +1 -1
  25. package/lib/module/components/experimental/Lists/DataList/actions/CopyAction.js +1 -1
  26. package/lib/module/components/experimental/Lists/DataList/actions/CopyAction.js.map +1 -1
  27. package/lib/module/components/experimental/Lists/DataList/actions/GenericAction.js +1 -1
  28. package/lib/module/components/experimental/Lists/DataList/actions/GenericAction.js.map +1 -1
  29. package/lib/module/components/exports.js +1 -1
  30. package/lib/module/components/exports.js.map +1 -1
  31. package/lib/module/components/primitives/F0Icon/F0Icon.js +2 -0
  32. package/lib/module/components/primitives/F0Icon/F0Icon.js.map +1 -0
  33. package/lib/module/components/primitives/F0Icon/F0Icon.md +187 -0
  34. package/lib/module/components/primitives/F0Icon/F0Icon.styles.js +2 -0
  35. package/lib/module/components/primitives/F0Icon/F0Icon.styles.js.map +1 -0
  36. package/lib/module/components/primitives/F0Icon/F0Icon.types.js +2 -0
  37. package/lib/module/components/primitives/F0Icon/F0Icon.types.js.map +1 -0
  38. package/lib/module/components/primitives/F0Icon/index.js +2 -0
  39. package/lib/module/components/primitives/F0Icon/index.js.map +1 -0
  40. package/lib/module/components/primitives/PressableFeedback/PressableFeedback.js +2 -0
  41. package/lib/module/components/primitives/PressableFeedback/PressableFeedback.js.map +1 -0
  42. package/lib/module/components/primitives/PressableFeedback/PressableFeedback.md +50 -0
  43. package/lib/module/components/primitives/PressableFeedback/PressableFeedback.types.js +2 -0
  44. package/lib/module/components/primitives/PressableFeedback/PressableFeedback.types.js.map +1 -0
  45. package/lib/module/components/primitives/PressableFeedback/index.js +2 -0
  46. package/lib/module/components/primitives/PressableFeedback/index.js.map +1 -0
  47. package/lib/module/icons/index.js +1 -1
  48. package/lib/module/icons/index.js.map +1 -1
  49. package/lib/typescript/components/Activity/ActivityItem/index.d.ts +1 -1
  50. package/lib/typescript/components/Activity/ActivityItem/index.d.ts.map +1 -1
  51. package/lib/typescript/components/Avatars/IconAvatar/index.d.ts +1 -1
  52. package/lib/typescript/components/Avatars/IconAvatar/index.d.ts.map +1 -1
  53. package/lib/typescript/components/Avatars/ModuleAvatar/index.d.ts +1 -1
  54. package/lib/typescript/components/Avatars/ModuleAvatar/index.d.ts.map +1 -1
  55. package/lib/typescript/components/Badge/index.d.ts +1 -1
  56. package/lib/typescript/components/Badge/index.d.ts.map +1 -1
  57. package/lib/typescript/components/Button/index.d.ts +1 -1
  58. package/lib/typescript/components/Button/index.d.ts.map +1 -1
  59. package/lib/typescript/components/Icon/index.d.ts +7 -14
  60. package/lib/typescript/components/Icon/index.d.ts.map +1 -1
  61. package/lib/typescript/components/OneChip/index.d.ts +1 -1
  62. package/lib/typescript/components/OneChip/index.d.ts.map +1 -1
  63. package/lib/typescript/components/Tags/RawTag/index.d.ts +1 -1
  64. package/lib/typescript/components/Tags/RawTag/index.d.ts.map +1 -1
  65. package/lib/typescript/components/experimental/Lists/DataList/ItemContainer.d.ts +1 -1
  66. package/lib/typescript/components/experimental/Lists/DataList/ItemContainer.d.ts.map +1 -1
  67. package/lib/typescript/components/experimental/Lists/DataList/actions/CopyAction.d.ts.map +1 -1
  68. package/lib/typescript/components/experimental/Lists/DataList/index.d.ts +1 -1
  69. package/lib/typescript/components/experimental/Lists/DataList/index.d.ts.map +1 -1
  70. package/lib/typescript/components/exports.d.ts +3 -2
  71. package/lib/typescript/components/exports.d.ts.map +1 -1
  72. package/lib/typescript/components/primitives/F0Icon/F0Icon.d.ts +25 -0
  73. package/lib/typescript/components/primitives/F0Icon/F0Icon.d.ts.map +1 -0
  74. package/lib/typescript/components/primitives/F0Icon/F0Icon.styles.d.ts +90 -0
  75. package/lib/typescript/components/primitives/F0Icon/F0Icon.styles.d.ts.map +1 -0
  76. package/lib/typescript/components/primitives/F0Icon/F0Icon.types.d.ts +47 -0
  77. package/lib/typescript/components/primitives/F0Icon/F0Icon.types.d.ts.map +1 -0
  78. package/lib/typescript/components/primitives/F0Icon/index.d.ts +10 -0
  79. package/lib/typescript/components/primitives/F0Icon/index.d.ts.map +1 -0
  80. package/lib/typescript/components/primitives/PressableFeedback/PressableFeedback.d.ts +18 -0
  81. package/lib/typescript/components/primitives/PressableFeedback/PressableFeedback.d.ts.map +1 -0
  82. package/lib/typescript/components/{PressableFeedback/index.d.ts → primitives/PressableFeedback/PressableFeedback.types.d.ts} +10 -7
  83. package/lib/typescript/components/primitives/PressableFeedback/PressableFeedback.types.d.ts.map +1 -0
  84. package/lib/typescript/components/primitives/PressableFeedback/index.d.ts +3 -0
  85. package/lib/typescript/components/primitives/PressableFeedback/index.d.ts.map +1 -0
  86. package/lib/typescript/icons/index.d.ts +0 -1
  87. package/lib/typescript/icons/index.d.ts.map +1 -1
  88. package/package.json +1 -1
  89. package/src/components/Activity/ActivityItem/index.spec.tsx +1 -1
  90. package/src/components/Activity/ActivityItem/index.tsx +1 -1
  91. package/src/components/Avatars/IconAvatar/index.tsx +6 -2
  92. package/src/components/Avatars/ModuleAvatar/index.tsx +1 -1
  93. package/src/components/Badge/index.tsx +2 -2
  94. package/src/components/Button/index.spec.tsx +3 -4
  95. package/src/components/Button/index.tsx +19 -13
  96. package/src/components/Icon/__tests__/Icon.spec.tsx +0 -4
  97. package/src/components/Icon/index.tsx +7 -26
  98. package/src/components/OneChip/index.tsx +4 -4
  99. package/src/components/OnePreset/index.tsx +1 -1
  100. package/src/components/Tags/AlertTab/index.tsx +2 -2
  101. package/src/components/Tags/BaseTag/index.tsx +1 -1
  102. package/src/components/Tags/RawTag/index.tsx +2 -2
  103. package/src/components/experimental/Lists/DataList/ItemContainer.tsx +2 -2
  104. package/src/components/experimental/Lists/DataList/actions/CopyAction.tsx +7 -10
  105. package/src/components/experimental/Lists/DataList/actions/GenericAction.tsx +2 -2
  106. package/src/components/experimental/Lists/DataList/index.tsx +1 -1
  107. package/src/components/experimental/Lists/DetailsItem/__snapshots__/index.spec.tsx.snap +4 -4
  108. package/src/components/experimental/Lists/DetailsItemsList/__snapshots__/index.spec.tsx.snap +1 -1
  109. package/src/components/exports.ts +3 -2
  110. package/src/components/primitives/F0Icon/F0Icon.md +187 -0
  111. package/src/components/primitives/F0Icon/F0Icon.styles.ts +43 -0
  112. package/src/components/primitives/F0Icon/F0Icon.tsx +73 -0
  113. package/src/components/primitives/F0Icon/F0Icon.types.ts +77 -0
  114. package/src/components/primitives/F0Icon/__tests__/F0Icon.spec.tsx +131 -0
  115. package/src/components/primitives/F0Icon/__tests__/F0Icon.tokens.spec.ts +39 -0
  116. package/src/components/primitives/F0Icon/index.ts +10 -0
  117. package/src/components/primitives/PressableFeedback/PressableFeedback.md +50 -0
  118. package/src/components/{PressableFeedback/index.tsx → primitives/PressableFeedback/PressableFeedback.tsx} +18 -44
  119. package/src/components/primitives/PressableFeedback/PressableFeedback.types.ts +45 -0
  120. package/src/components/{PressableFeedback → primitives/PressableFeedback/__tests__}/index.spec.tsx +1 -5
  121. package/src/components/primitives/PressableFeedback/index.ts +7 -0
  122. package/src/icons/index.ts +0 -1
  123. package/lib/module/components/Icon/README.md +0 -63
  124. package/lib/module/components/PressableFeedback/index.js +0 -2
  125. package/lib/module/components/PressableFeedback/index.js.map +0 -1
  126. package/lib/module/components/PressableFeedback/index.spec.js +0 -2
  127. package/lib/module/components/PressableFeedback/index.spec.js.map +0 -1
  128. package/lib/typescript/components/PressableFeedback/index.d.ts.map +0 -1
  129. package/src/components/Icon/README.md +0 -63
  130. /package/src/components/{PressableFeedback → primitives/PressableFeedback/__tests__}/__snapshots__/index.spec.tsx.snap +0 -0
@@ -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,11 +5,10 @@ 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"
12
- export * from "./PressableFeedback"
13
12
  export * from "./Tags/exports"
14
13
  export * from "./experimental/Lists/DataList"
15
14
  export * from "./experimental/Lists/DetailsItem"
@@ -17,3 +16,5 @@ export * from "./experimental/Lists/DetailsItemsList"
17
16
 
18
17
  // Export primitives
19
18
  export * from "./primitives/F0Text"
19
+ export * from "./primitives/F0Icon"
20
+ export * from "./primitives/PressableFeedback"
@@ -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
@@ -0,0 +1,77 @@
1
+ import type { ForwardRefExoticComponent, RefAttributes } from "react"
2
+ import type { SvgProps } from "react-native-svg"
3
+ import type { Svg } from "react-native-svg"
4
+ import type { VariantProps } from "tailwind-variants"
5
+
6
+ import type { iconVariants } from "./F0Icon.styles"
7
+
8
+ /**
9
+ * Icon component type - forward ref to SVG component with className support
10
+ */
11
+ export type IconType = ForwardRefExoticComponent<
12
+ SvgProps &
13
+ RefAttributes<Svg> & {
14
+ className?: string
15
+ }
16
+ >
17
+
18
+ /**
19
+ * Icon color variants derived from f0-icon-* tokens in src/styles/theme.css
20
+ * Sync is enforced by F0Icon.tokens.spec.ts
21
+ */
22
+ export const ICON_COLORS = [
23
+ "default",
24
+ "secondary",
25
+ "inverse",
26
+ "bold",
27
+ "critical",
28
+ "critical-bold",
29
+ "accent",
30
+ "info",
31
+ "warning",
32
+ "positive",
33
+ "promote",
34
+ "selected",
35
+ "selected-hover",
36
+ "mood-super-negative",
37
+ "mood-negative",
38
+ "mood-neutral",
39
+ "mood-positive",
40
+ "mood-super-positive",
41
+ ] as const
42
+
43
+ export type IconColor = (typeof ICON_COLORS)[number]
44
+
45
+ /**
46
+ * Public F0Icon props
47
+ * Supports semantic color via `color` prop, with `className` as escape hatch.
48
+ */
49
+ export interface F0IconProps extends Omit<SvgProps, "style"> {
50
+ /**
51
+ * Tailwind className for custom styling or color overrides.
52
+ * Prefer the `color` prop for semantic icon colors.
53
+ */
54
+ className?: string
55
+
56
+ /**
57
+ * Semantic icon color from the F0 design system
58
+ * Maps to f0-icon-* tokens (e.g. color="critical" -> text-f0-icon-critical)
59
+ */
60
+ color?: IconColor
61
+
62
+ /**
63
+ * Icon component to render (from icons directory)
64
+ */
65
+ icon: IconType
66
+
67
+ /**
68
+ * Icon size variant
69
+ * @default 'md'
70
+ */
71
+ size?: VariantProps<typeof iconVariants>["size"]
72
+
73
+ /**
74
+ * Test ID for testing
75
+ */
76
+ testID?: string
77
+ }
@@ -0,0 +1,131 @@
1
+ import { render } from "@testing-library/react-native"
2
+ import React from "react"
3
+
4
+ import { Archive } from "../../../../icons/app"
5
+ import { Home } from "../../../../icons/modules"
6
+ import { applyIconInterop } from "../F0Icon"
7
+ import F0Icon from "../F0Icon"
8
+
9
+ describe("F0Icon", () => {
10
+ it("renders correctly with an app icon", () => {
11
+ const { getByTestId } = render(<F0Icon icon={Archive} testID="icon" />)
12
+ expect(getByTestId("icon")).toBeTruthy()
13
+ })
14
+
15
+ it("renders correctly with a module icon", () => {
16
+ const { getByTestId } = render(<F0Icon icon={Home} testID="icon" />)
17
+ expect(getByTestId("icon")).toBeTruthy()
18
+ })
19
+
20
+ it("applies the correct size variant", () => {
21
+ const { getByTestId } = render(
22
+ <F0Icon icon={Archive} size="lg" testID="icon" />
23
+ )
24
+ expect(getByTestId("icon")).toBeTruthy()
25
+ })
26
+
27
+ it("returns null when no icon is provided", () => {
28
+ // @ts-expect-error - Testing runtime behavior
29
+ const { queryByTestId } = render(<F0Icon testID="icon" />)
30
+ expect(queryByTestId("icon")).toBeNull()
31
+ })
32
+
33
+ it("applies default size when size prop is not provided", () => {
34
+ const { getByTestId } = render(<F0Icon icon={Archive} testID="icon" />)
35
+ expect(getByTestId("icon")).toBeTruthy()
36
+ })
37
+
38
+ it("forwards ref correctly", () => {
39
+ const ref = React.createRef<React.ElementRef<typeof F0Icon>>()
40
+ render(<F0Icon icon={Archive} ref={ref} testID="icon" />)
41
+ expect(ref.current).toBeTruthy()
42
+ })
43
+
44
+ it("renders with a color prop", () => {
45
+ const { getByTestId } = render(
46
+ <F0Icon icon={Archive} color="critical" testID="icon" />
47
+ )
48
+ expect(getByTestId("icon")).toBeTruthy()
49
+ })
50
+
51
+ it("caches wrapped icon: applyIconInterop returns same instance for same icon", () => {
52
+ const wrapped1 = applyIconInterop(Archive)
53
+ const wrapped2 = applyIconInterop(Archive)
54
+ expect(wrapped1).toBe(wrapped2)
55
+ })
56
+
57
+ it("caches per icon type: different icons return different wrapped instances", () => {
58
+ const wrappedArchive = applyIconInterop(Archive)
59
+ const wrappedHome = applyIconInterop(Home)
60
+ expect(wrappedArchive).not.toBe(wrappedHome)
61
+ })
62
+
63
+ it("renders with both color and className", () => {
64
+ const { getByTestId } = render(
65
+ <F0Icon icon={Archive} color="info" className="-ml-0.5" testID="icon" />
66
+ )
67
+ expect(getByTestId("icon")).toBeTruthy()
68
+ })
69
+
70
+ describe("className — cn() merging", () => {
71
+ it("passes through layout classes from className", () => {
72
+ const { getByTestId } = render(
73
+ <F0Icon icon={Archive} className="-ml-0.5" testID="icon" />
74
+ )
75
+ const element = getByTestId("icon")
76
+ expect(element.props.className).toContain("-ml-0.5")
77
+ })
78
+
79
+ it("merges conflicting size: custom w-* overrides variant", () => {
80
+ const { getByTestId } = render(
81
+ <F0Icon icon={Archive} size="md" className="h-10 w-10" testID="icon" />
82
+ )
83
+ const element = getByTestId("icon")
84
+ expect(element.props.className).toContain("w-10")
85
+ expect(element.props.className).toContain("h-10")
86
+ expect(element.props.className).not.toContain("w-5")
87
+ expect(element.props.className).not.toContain("h-5")
88
+ })
89
+
90
+ it("merges conflicting color: custom text-* overrides variant", () => {
91
+ const { getByTestId } = render(
92
+ <F0Icon
93
+ icon={Archive}
94
+ color="critical"
95
+ className="text-red-500"
96
+ testID="icon"
97
+ />
98
+ )
99
+ const element = getByTestId("icon")
100
+ expect(element.props.className).toContain("text-red-500")
101
+ expect(element.props.className).not.toContain("text-f0-icon-critical")
102
+ })
103
+
104
+ it("uses only variant classes when className is undefined", () => {
105
+ const { getByTestId } = render(
106
+ <F0Icon icon={Archive} size="sm" color="positive" testID="icon" />
107
+ )
108
+ const element = getByTestId("icon")
109
+ expect(element.props.className).toContain("w-4")
110
+ expect(element.props.className).toContain("h-4")
111
+ expect(element.props.className).toContain("text-f0-icon-positive")
112
+ })
113
+
114
+ it("combines non-conflicting variant and className", () => {
115
+ const { getByTestId } = render(
116
+ <F0Icon
117
+ icon={Archive}
118
+ size="lg"
119
+ color="info"
120
+ className="opacity-50"
121
+ testID="icon"
122
+ />
123
+ )
124
+ const element = getByTestId("icon")
125
+ expect(element.props.className).toContain("w-6")
126
+ expect(element.props.className).toContain("h-6")
127
+ expect(element.props.className).toContain("text-f0-icon-info")
128
+ expect(element.props.className).toContain("opacity-50")
129
+ })
130
+ })
131
+ })
@@ -0,0 +1,39 @@
1
+ // @ts-ignore - Node built-ins are available in Jest runtime
2
+ import fs from "fs"
3
+ // @ts-ignore - Node built-ins are available in Jest runtime
4
+ import path from "path"
5
+
6
+ import { ICON_COLORS } from "../F0Icon.types"
7
+
8
+ describe("F0Icon token sync", () => {
9
+ it("ICON_COLORS matches f0-icon-* tokens in theme.css", () => {
10
+ const css = fs.readFileSync(
11
+ // @ts-ignore - __dirname is available in Jest runtime
12
+ path.resolve(__dirname, "../../../../styles/theme.css"),
13
+ "utf-8"
14
+ )
15
+
16
+ // Extract all --color-f0-icon-<name> tokens, excluding sub-tokens
17
+ // that are already captured (e.g. --color-f0-icon-mood-positive
18
+ // but not --color-f0-icon which maps to "default")
19
+ const tokenRegex = /--color-f0-icon-([a-z0-9][a-z0-9-]*):/g
20
+ const tokensFromCSS = new Set<string>()
21
+ let match: RegExpExecArray | null
22
+ while ((match = tokenRegex.exec(css)) !== null) {
23
+ tokensFromCSS.add(match[1])
24
+ }
25
+
26
+ // "default" in ICON_COLORS maps to the base --color-f0-icon token
27
+ const colorsFromType = new Set<string>(
28
+ ICON_COLORS.filter((c) => c !== "default")
29
+ )
30
+
31
+ const missingInType = [...tokensFromCSS].filter(
32
+ (t) => !colorsFromType.has(t)
33
+ )
34
+ const extraInType = [...colorsFromType].filter((t) => !tokensFromCSS.has(t))
35
+
36
+ expect(missingInType).toEqual([])
37
+ expect(extraInType).toEqual([])
38
+ })
39
+ })
@@ -0,0 +1,10 @@
1
+ /**
2
+ * F0Icon - Icon primitive component
3
+ *
4
+ * @see F0Icon.md for documentation
5
+ */
6
+
7
+ export { default as F0Icon } from "./F0Icon"
8
+ export type { F0IconProps, IconType, IconColor } from "./F0Icon.types"
9
+ export { ICON_COLORS } from "./F0Icon.types"
10
+ export { applyIconInterop } from "./F0Icon"