@factorialco/f0-react-native 0.28.1 → 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 (133) 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/components/primitives/F0Text/AnimatedF0Text/AnimatedF0Text.js +1 -1
  37. package/lib/module/components/primitives/F0Text/AnimatedF0Text/AnimatedF0Text.js.map +1 -1
  38. package/lib/module/components/primitives/F0Text/AnimatedF0Text/AnimatedF0Text.md +45 -8
  39. package/lib/module/components/primitives/F0Text/F0Text/F0Text.js +1 -1
  40. package/lib/module/components/primitives/F0Text/F0Text/F0Text.js.map +1 -1
  41. package/lib/module/components/primitives/F0Text/F0Text/F0Text.md +42 -31
  42. package/lib/module/components/primitives/F0Text/F0Text/F0Text.styles.js +1 -1
  43. package/lib/module/components/primitives/F0Text/F0Text/F0Text.styles.js.map +1 -1
  44. package/lib/module/components/primitives/F0Text/F0Text/F0Text.types.js +1 -1
  45. package/lib/module/components/primitives/F0Text/F0Text/F0Text.types.js.map +1 -1
  46. package/lib/module/icons/index.js +1 -1
  47. package/lib/module/icons/index.js.map +1 -1
  48. package/lib/module/lib/utils.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 +2 -1
  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/F0Text/AnimatedF0Text/AnimatedF0Text.d.ts.map +1 -1
  81. package/lib/typescript/components/primitives/F0Text/AnimatedF0Text/AnimatedF0Text.types.d.ts +14 -0
  82. package/lib/typescript/components/primitives/F0Text/AnimatedF0Text/AnimatedF0Text.types.d.ts.map +1 -1
  83. package/lib/typescript/components/primitives/F0Text/F0Text/F0Text.d.ts.map +1 -1
  84. package/lib/typescript/components/primitives/F0Text/F0Text/F0Text.styles.d.ts +2 -2
  85. package/lib/typescript/components/primitives/F0Text/F0Text/F0Text.styles.d.ts.map +1 -1
  86. package/lib/typescript/components/primitives/F0Text/F0Text/F0Text.types.d.ts +22 -18
  87. package/lib/typescript/components/primitives/F0Text/F0Text/F0Text.types.d.ts.map +1 -1
  88. package/lib/typescript/icons/index.d.ts +0 -1
  89. package/lib/typescript/icons/index.d.ts.map +1 -1
  90. package/lib/typescript/lib/utils.d.ts +1 -2
  91. package/lib/typescript/lib/utils.d.ts.map +1 -1
  92. package/package.json +1 -1
  93. package/src/components/Activity/ActivityItem/index.spec.tsx +1 -1
  94. package/src/components/Activity/ActivityItem/index.tsx +1 -1
  95. package/src/components/Avatars/IconAvatar/index.tsx +6 -2
  96. package/src/components/Avatars/ModuleAvatar/index.tsx +1 -1
  97. package/src/components/Badge/index.tsx +2 -2
  98. package/src/components/Button/index.spec.tsx +3 -4
  99. package/src/components/Button/index.tsx +19 -13
  100. package/src/components/Icon/__tests__/Icon.spec.tsx +0 -4
  101. package/src/components/Icon/index.tsx +7 -26
  102. package/src/components/OneChip/index.tsx +3 -3
  103. package/src/components/Tags/AlertTab/index.tsx +2 -2
  104. package/src/components/Tags/RawTag/index.tsx +2 -2
  105. package/src/components/experimental/Lists/DataList/ItemContainer.tsx +2 -2
  106. package/src/components/experimental/Lists/DataList/actions/CopyAction.tsx +7 -10
  107. package/src/components/experimental/Lists/DataList/actions/GenericAction.tsx +2 -2
  108. package/src/components/experimental/Lists/DataList/index.tsx +1 -1
  109. package/src/components/experimental/Lists/DetailsItem/__snapshots__/index.spec.tsx.snap +4 -4
  110. package/src/components/experimental/Lists/DetailsItemsList/__snapshots__/index.spec.tsx.snap +1 -1
  111. package/src/components/exports.ts +2 -1
  112. package/src/components/primitives/F0Icon/F0Icon.md +187 -0
  113. package/src/components/primitives/F0Icon/F0Icon.styles.ts +43 -0
  114. package/src/components/primitives/F0Icon/F0Icon.tsx +73 -0
  115. package/src/components/primitives/F0Icon/F0Icon.types.ts +77 -0
  116. package/src/components/primitives/F0Icon/__tests__/F0Icon.spec.tsx +131 -0
  117. package/src/components/primitives/F0Icon/__tests__/F0Icon.tokens.spec.ts +39 -0
  118. package/src/components/primitives/F0Icon/index.ts +10 -0
  119. package/src/components/primitives/F0Text/AnimatedF0Text/AnimatedF0Text.md +45 -8
  120. package/src/components/primitives/F0Text/AnimatedF0Text/AnimatedF0Text.tsx +20 -8
  121. package/src/components/primitives/F0Text/AnimatedF0Text/AnimatedF0Text.types.ts +15 -0
  122. package/src/components/primitives/F0Text/AnimatedF0Text/__tests__/AnimatedF0Text.spec.tsx +220 -0
  123. package/src/components/primitives/F0Text/AnimatedF0Text/__tests__/__snapshots__/AnimatedF0Text.spec.tsx.snap +16 -16
  124. package/src/components/primitives/F0Text/F0Text/F0Text.md +42 -31
  125. package/src/components/primitives/F0Text/F0Text/F0Text.styles.ts +1 -1
  126. package/src/components/primitives/F0Text/F0Text/F0Text.tsx +17 -10
  127. package/src/components/primitives/F0Text/F0Text/F0Text.types.ts +22 -18
  128. package/src/components/primitives/F0Text/F0Text/__tests__/F0Text.spec.tsx +340 -16
  129. package/src/components/primitives/F0Text/F0Text/__tests__/__snapshots__/F0Text.spec.tsx.snap +36 -36
  130. package/src/icons/index.ts +0 -1
  131. package/src/lib/utils.ts +1 -2
  132. package/lib/module/components/Icon/README.md +0 -63
  133. package/src/components/Icon/README.md +0 -63
@@ -40,18 +40,19 @@ import { F0Text } from "@factorialco/f0-react-native"
40
40
 
41
41
  ### Props
42
42
 
43
- | Prop | Type | Default | Description |
44
- | --------------- | ------------------- | ------------------- | ------------------------------------------------------- |
45
- | `variant` | `TypographyVariant` | `'body-sm-default'` | Typography variant with weight included |
46
- | `color` | `TextColor` | `'default'` | Text color from F0 semantic color system |
47
- | `align` | `TextAlign` | `'left'` | Text alignment (left, center, right, justify) |
48
- | `decoration` | `TextDecoration` | `'none'` | Text decoration (none, underline, line-through) |
49
- | `transform` | `TextTransform` | `'none'` | Text transform (none, uppercase, lowercase, capitalize) |
50
- | `numberOfLines` | `number` | `undefined` | Max lines before truncation with ellipsis |
43
+ | Prop | Type | Default | Description |
44
+ | --------------- | ------------------- | ------------------- | -------------------------------------------------------- |
45
+ | `variant` | `TypographyVariant` | `'body-sm-default'` | Typography variant with weight included |
46
+ | `color` | `TextColor` | `'default'` | Text color from F0 semantic color system |
47
+ | `align` | `TextAlign` | `'left'` | Text alignment (left, center, right, justify) |
48
+ | `decoration` | `TextDecoration` | `'none'` | Text decoration (none, underline, line-through) |
49
+ | `transform` | `TextTransform` | `'none'` | Text transform (none, uppercase, lowercase, capitalize) |
50
+ | `numberOfLines` | `number` | `undefined` | Max lines before truncation with ellipsis |
51
+ | `className` | `string` | `undefined` | Layout/positioning classes (margin, padding, flex, etc.) |
51
52
 
52
- All React Native `TextProps` are also supported (onPress, testID, etc.).
53
+ All React Native `TextProps` are also supported (onPress, testID, etc.), **except `style`** which is omitted from the type and filtered at runtime.
53
54
 
54
- **Note**: `className` and `style` props are **not available**. Use semantic props for typography. For spacing/layout, wrap F0Text in a View. Both props are filtered at runtime to prevent override via spread.
55
+ Typography is controlled exclusively by semantic props (variant, color, align, etc.) and always takes precedence any typography classes passed via `className` are automatically overridden by the semantic props via `twMerge`.
55
56
 
56
57
  ### Typography Variants
57
58
 
@@ -180,26 +181,33 @@ All variants use **Inter** font family with the weight included in the variant n
180
181
 
181
182
  ### Spacing & Layout
182
183
 
183
- F0Text doesn't accept `className` to prevent typography override. Use a View wrapper for spacing:
184
+ F0Text accepts `className` for layout and positioning. Typography classes in `className` are safely overridden by semantic props via `twMerge`:
184
185
 
185
186
  <!-- prettier-ignore -->
186
187
  ```tsx
187
188
  <>
188
- {/* Spacing with View wrapper */}
189
- <View className="mt-4 mb-2">
190
- <F0Text variant="body-sm-default">Text with margin</F0Text>
191
- </View>
189
+ {/* Spacing directly on the text */}
190
+ <F0Text variant="body-sm-default" className="mt-4 mb-2">
191
+ Text with margin
192
+ </F0Text>
192
193
 
193
- {/* Layout with View wrapper */}
194
- <View className="flex-1">
195
- <F0Text variant="body-sm-default">Flexible text</F0Text>
196
- </View>
194
+ {/* Layout directly on the text */}
195
+ <F0Text variant="body-sm-default" className="flex-1">
196
+ Flexible text
197
+ </F0Text>
197
198
 
198
199
  {/* Icon + Text pattern */}
199
200
  <View className="flex-row items-center gap-2">
200
201
  <F0Icon icon={Check} size="sm" />
201
- <F0Text variant="body-sm-default">Success message</F0Text>
202
+ <F0Text variant="body-sm-default" className="flex-1">
203
+ Success message
204
+ </F0Text>
202
205
  </View>
206
+
207
+ {/* Typography override attempts are safely ignored */}
208
+ <F0Text variant="body-sm-default" className="mt-4 font-bold text-red-500">
209
+ font-bold and text-red-500 are ignored; mt-4 is applied
210
+ </F0Text>
203
211
  </>
204
212
  ```
205
213
 
@@ -236,18 +244,16 @@ F0Text doesn't accept `className` to prevent typography override. Use a View wra
236
244
  <!-- prettier-ignore -->
237
245
  ```tsx
238
246
  <View className="rounded-lg bg-f0-background-secondary p-4">
239
- <View className="mb-2">
240
- <F0Text variant="heading-sm">Card Title</F0Text>
241
- </View>
247
+ <F0Text variant="heading-sm" className="mb-2">
248
+ Card Title
249
+ </F0Text>
242
250
  <F0Text variant="body-sm-default" color="secondary" numberOfLines={2}>
243
251
  This is a description that will be truncated after two lines if it's too
244
252
  long to fit in the available space.
245
253
  </F0Text>
246
- <View className="mt-2">
247
- <F0Text variant="body-xs-medium" color="tertiary">
248
- Last updated 2 hours ago
249
- </F0Text>
250
- </View>
254
+ <F0Text variant="body-xs-medium" color="tertiary" className="mt-2">
255
+ Last updated 2 hours ago
256
+ </F0Text>
251
257
  </View>
252
258
  ```
253
259
 
@@ -347,11 +353,16 @@ to these variables.
347
353
 
348
354
  <!-- prettier-ignore -->
349
355
  ```tsx
350
- // ✅ Good: Use appropriate variant
356
+ // ✅ Good: Use semantic props for typography
351
357
  <F0Text variant="body-md-semibold">Bold text</F0Text>
352
358
 
353
- // Bad: Don't try to override with className (not supported)
354
- // <F0Text className="font-bold">Text</F0Text>
359
+ // Good: Use className for layout
360
+ <F0Text variant="body-md-semibold" className="mt-4 flex-1">Bold text</F0Text>
361
+
362
+ // ❌ Bad: Don't use className for typography (it will be overridden)
363
+ <F0Text variant="body-sm-default" className="font-bold text-red-500">
364
+ font-bold and text-red-500 are silently ignored
365
+ </F0Text>
355
366
  ```
356
367
 
357
368
  <!-- prettier-ignore -->
@@ -7,7 +7,7 @@ import { tv, type VariantProps } from "tailwind-variants"
7
7
  * via --font-* CSS variables defined in @theme.
8
8
  */
9
9
  export const textVariants = tv({
10
- base: "",
10
+ base: "no-underline normal-case tracking-normal",
11
11
  variants: {
12
12
  variant: {
13
13
  // Heading variants
@@ -1,7 +1,7 @@
1
1
  import React from "react"
2
2
  import { Text as RNText } from "react-native"
3
3
 
4
- import { omitProps } from "../../../../lib/utils"
4
+ import { cn, omitProps } from "../../../../lib/utils"
5
5
 
6
6
  import { textVariants } from "./F0Text.styles"
7
7
  import { F0_TEXT_BANNED_PROPS, type F0TextProps } from "./F0Text.types"
@@ -9,10 +9,13 @@ import { F0_TEXT_BANNED_PROPS, type F0TextProps } from "./F0Text.types"
9
9
  /**
10
10
  * F0Text - Primitive Text component with semantic typography variants
11
11
  *
12
+ * Typography is controlled by semantic props and always takes precedence.
13
+ * `className` is accepted for layout/positioning (margin, padding, flex, etc.).
14
+ *
12
15
  * @example
13
16
  * <F0Text variant="heading-lg">Large Heading</F0Text>
14
17
  * <F0Text variant="body-sm-default" color="secondary">Secondary text</F0Text>
15
- * <F0Text variant="body-md-medium" numberOfLines={2}>Truncated text...</F0Text>
18
+ * <F0Text variant="body-sm-default" className="mt-4 flex-1">Positioned text</F0Text>
16
19
  */
17
20
  const F0TextComponent = React.forwardRef<RNText, F0TextProps>(
18
21
  (
@@ -22,6 +25,7 @@ const F0TextComponent = React.forwardRef<RNText, F0TextProps>(
22
25
  align = "left",
23
26
  decoration = "none",
24
27
  transform = "none",
28
+ className,
25
29
  children,
26
30
  numberOfLines,
27
31
  ...rest
@@ -30,14 +34,17 @@ const F0TextComponent = React.forwardRef<RNText, F0TextProps>(
30
34
  ) => {
31
35
  const textClassName = React.useMemo(
32
36
  () =>
33
- textVariants({
34
- variant,
35
- color,
36
- align,
37
- decoration,
38
- transform,
39
- }),
40
- [variant, color, align, decoration, transform]
37
+ cn(
38
+ className,
39
+ textVariants({
40
+ variant,
41
+ color,
42
+ align,
43
+ decoration,
44
+ transform,
45
+ })
46
+ ),
47
+ [variant, color, align, decoration, transform, className]
41
48
  )
42
49
 
43
50
  return (
@@ -1,11 +1,11 @@
1
1
  import type { TextProps as RNTextProps } from "react-native"
2
2
 
3
3
  /**
4
- * Props that must not be passed through to the underlying RN Text
5
- * (`style` and `className` are handled by F0 instead).
6
- * Used with omitProps for runtime safety.
4
+ * Props that must not be passed through to the underlying RN Text.
5
+ * `style` is blocked to enforce the semantic API; `className` is allowed
6
+ * for layout/positioning and merged with typography classes via twMerge.
7
7
  */
8
- export const F0_TEXT_BANNED_PROPS = ["style", "className"] as const
8
+ export const F0_TEXT_BANNED_PROPS = ["style"] as const
9
9
 
10
10
  /**
11
11
  * Typography variant types based on semantic design tokens
@@ -73,10 +73,14 @@ export const TEXT_TRANSFORMS = [
73
73
  export type TextTransform = (typeof TEXT_TRANSFORMS)[number]
74
74
 
75
75
  /**
76
- * Internal props for the F0Text component.
77
- * @private
76
+ * Props for the F0Text component.
77
+ *
78
+ * `className` is available for layout/positioning (margin, padding, flex, etc.).
79
+ * Typography is controlled exclusively by semantic props (variant, color, align, etc.)
80
+ * and always takes precedence — any typography classes in `className` are overridden.
81
+ * `style` is NOT available (omitted from RNTextProps and filtered at runtime).
78
82
  */
79
- interface F0TextPropsInternal extends Omit<RNTextProps, "style"> {
83
+ export interface F0TextProps extends Omit<RNTextProps, "style"> {
80
84
  /**
81
85
  * Semantic typography variant
82
86
  * @default "body-sm-default"
@@ -118,17 +122,17 @@ interface F0TextPropsInternal extends Omit<RNTextProps, "style"> {
118
122
  children?: React.ReactNode
119
123
 
120
124
  /**
121
- * Excluded from public API via Omit<F0TextPropsInternal, "className">.
122
- * @private
125
+ * Tailwind classes for layout and positioning.
126
+ *
127
+ * Allowed: margin, padding, flex, position, width, height, opacity, z-index, etc.
128
+ * Ignored: font-size, font-weight, line-height, letter-spacing, color, text-align,
129
+ * text-decoration, text-transform — these are controlled by semantic props and
130
+ * always take precedence via twMerge.
131
+ *
132
+ * @example
133
+ * className="mt-4 flex-1"
134
+ * className="mb-2 self-center"
135
+ * className="absolute top-0 left-0"
123
136
  */
124
137
  className?: string
125
138
  }
126
-
127
- /**
128
- * Public props for the F0Text component
129
- *
130
- * Note: `className` and `style` props are NOT available.
131
- * Use semantic props (variant, color, align, etc.) for typography.
132
- * For spacing/layout, wrap F0Text in a View with className.
133
- */
134
- export type F0TextProps = Omit<F0TextPropsInternal, "className">
@@ -142,6 +142,7 @@ describe("F0Text", () => {
142
142
  )
143
143
  const element = getByText("Underlined text")
144
144
  expect(element.props.className).toContain("underline")
145
+ expect(element.props.className).not.toContain("no-underline")
145
146
  })
146
147
 
147
148
  it("applies transform classes", () => {
@@ -150,6 +151,7 @@ describe("F0Text", () => {
150
151
  )
151
152
  const element = getByText("uppercase text")
152
153
  expect(element.props.className).toContain("uppercase")
154
+ expect(element.props.className).not.toContain("normal-case")
153
155
  })
154
156
 
155
157
  it("sets numberOfLines prop correctly", () => {
@@ -193,36 +195,358 @@ describe("F0Text", () => {
193
195
  expect(element.props.style).toBeUndefined()
194
196
  })
195
197
 
196
- it("filters className prop at runtime (prevents override via spread)", () => {
197
- const propsWithClassName = {
198
- className: "font-bold text-red-500",
199
- children: "Text with className attempt",
200
- }
198
+ it("handles ref forwarding", () => {
199
+ const ref = React.createRef<RNText>()
200
+ render(<F0Text ref={ref}>Ref text</F0Text>)
201
+ expect(ref.current).toBeTruthy()
202
+ })
203
+
204
+ it("renders nested children", () => {
205
+ const { getByText } = render(
206
+ <F0Text>
207
+ Parent <F0Text variant="body-xs-medium">nested</F0Text> text
208
+ </F0Text>
209
+ )
210
+ expect(getByText("nested")).toBeTruthy()
211
+ })
212
+ })
213
+
214
+ describe("className — layout classes pass through", () => {
215
+ it("applies margin classes", () => {
216
+ const { getByText } = render(
217
+ <F0Text className="mx-3 mt-4 mb-2">Margin text</F0Text>
218
+ )
219
+ const element = getByText("Margin text")
220
+ expect(element.props.className).toContain("mt-4")
221
+ expect(element.props.className).toContain("mb-2")
222
+ expect(element.props.className).toContain("mx-3")
223
+ })
224
+
225
+ it("applies padding classes", () => {
226
+ const { getByText } = render(
227
+ <F0Text className="p-4 px-2 py-1">Padded text</F0Text>
228
+ )
229
+ const element = getByText("Padded text")
230
+ expect(element.props.className).toContain("p-4")
231
+ expect(element.props.className).toContain("px-2")
232
+ expect(element.props.className).toContain("py-1")
233
+ })
234
+
235
+ it("applies flex classes", () => {
236
+ const { getByText } = render(
237
+ <F0Text className="flex-1 flex-shrink-0">Flex text</F0Text>
238
+ )
239
+ const element = getByText("Flex text")
240
+ expect(element.props.className).toContain("flex-1")
241
+ expect(element.props.className).toContain("flex-shrink-0")
242
+ })
243
+
244
+ it("applies self-alignment classes", () => {
245
+ const { getByText } = render(
246
+ <F0Text className="self-center">Self-aligned text</F0Text>
247
+ )
248
+ const element = getByText("Self-aligned text")
249
+ expect(element.props.className).toContain("self-center")
250
+ })
251
+
252
+ it("applies position classes", () => {
253
+ const { getByText } = render(
254
+ <F0Text className="absolute top-0 left-0">Positioned text</F0Text>
255
+ )
256
+ const element = getByText("Positioned text")
257
+ expect(element.props.className).toContain("absolute")
258
+ expect(element.props.className).toContain("top-0")
259
+ expect(element.props.className).toContain("left-0")
260
+ })
261
+
262
+ it("applies width and height classes", () => {
263
+ const { getByText } = render(
264
+ <F0Text className="w-full max-w-xs">Sized text</F0Text>
265
+ )
266
+ const element = getByText("Sized text")
267
+ expect(element.props.className).toContain("w-full")
268
+ expect(element.props.className).toContain("max-w-xs")
269
+ })
270
+
271
+ it("applies opacity classes", () => {
272
+ const { getByText } = render(
273
+ <F0Text className="opacity-50">Faded text</F0Text>
274
+ )
275
+ const element = getByText("Faded text")
276
+ expect(element.props.className).toContain("opacity-50")
277
+ })
278
+
279
+ it("applies display/visibility classes", () => {
280
+ const { getByText } = render(
281
+ <F0Text className="hidden">Hidden text</F0Text>
282
+ )
283
+ const element = getByText("Hidden text")
284
+ expect(element.props.className).toContain("hidden")
285
+ })
286
+
287
+ it("applies z-index classes", () => {
288
+ const { getByText } = render(
289
+ <F0Text className="z-10">Layered text</F0Text>
290
+ )
291
+ const element = getByText("Layered text")
292
+ expect(element.props.className).toContain("z-10")
293
+ })
294
+
295
+ it("preserves layout classes with all variant combinations", () => {
296
+ const { getByText } = render(
297
+ <F0Text
298
+ variant="heading-lg"
299
+ color="critical"
300
+ align="center"
301
+ decoration="underline"
302
+ transform="uppercase"
303
+ className="mt-4 flex-1 self-end"
304
+ >
305
+ Full combo
306
+ </F0Text>
307
+ )
308
+ const element = getByText("Full combo")
309
+ expect(element.props.className).toContain("mt-4")
310
+ expect(element.props.className).toContain("flex-1")
311
+ expect(element.props.className).toContain("self-end")
312
+ expect(element.props.className).toContain("text-[24px]")
313
+ expect(element.props.className).toContain("font-semibold")
314
+ expect(element.props.className).toContain("text-f0-foreground-critical")
315
+ expect(element.props.className).toContain("text-center")
316
+ expect(element.props.className).toContain("underline")
317
+ expect(element.props.className).toContain("uppercase")
318
+ })
319
+ })
320
+
321
+ describe("className — typography overrides are rejected", () => {
322
+ it("rejects font-size override", () => {
323
+ const { getByText } = render(
324
+ <F0Text variant="body-sm-default" className="text-xs">
325
+ Size override attempt
326
+ </F0Text>
327
+ )
328
+ const element = getByText("Size override attempt")
329
+ expect(element.props.className).toContain("text-[14px]")
330
+ expect(element.props.className).not.toContain("text-xs")
331
+ })
332
+
333
+ it("rejects arbitrary font-size override", () => {
334
+ const { getByText } = render(
335
+ <F0Text variant="body-sm-default" className="text-[40px]">
336
+ Size override attempt
337
+ </F0Text>
338
+ )
339
+ const element = getByText("Size override attempt")
340
+ expect(element.props.className).toContain("text-[14px]")
341
+ expect(element.props.className).not.toContain("text-[40px]")
342
+ })
343
+
344
+ it("rejects font-weight override", () => {
345
+ const { getByText } = render(
346
+ <F0Text variant="body-sm-default" className="font-bold">
347
+ Weight override attempt
348
+ </F0Text>
349
+ )
350
+ const element = getByText("Weight override attempt")
351
+ expect(element.props.className).toContain("font-normal")
352
+ expect(element.props.className).not.toContain("font-bold")
353
+ })
354
+
355
+ it("rejects line-height override", () => {
356
+ const { getByText } = render(
357
+ <F0Text variant="body-sm-default" className="leading-10">
358
+ Leading override attempt
359
+ </F0Text>
360
+ )
361
+ const element = getByText("Leading override attempt")
362
+ expect(element.props.className).toContain("leading-[20px]")
363
+ expect(element.props.className).not.toContain("leading-10")
364
+ })
365
+
366
+ it("rejects letter-spacing override", () => {
367
+ const { getByText } = render(
368
+ <F0Text variant="heading-lg" className="tracking-widest">
369
+ Tracking override attempt
370
+ </F0Text>
371
+ )
372
+ const element = getByText("Tracking override attempt")
373
+ expect(element.props.className).toContain("tracking-[-0.2px]")
374
+ expect(element.props.className).not.toContain("tracking-widest")
375
+ })
376
+
377
+ it("rejects text-color override", () => {
378
+ const { getByText } = render(
379
+ <F0Text color="default" className="text-red-500">
380
+ Color override attempt
381
+ </F0Text>
382
+ )
383
+ const element = getByText("Color override attempt")
384
+ expect(element.props.className).toContain("text-f0-foreground")
385
+ expect(element.props.className).not.toContain("text-red-500")
386
+ })
387
+
388
+ it("rejects text-align override", () => {
389
+ const { getByText } = render(
390
+ <F0Text align="left" className="text-center">
391
+ Align override attempt
392
+ </F0Text>
393
+ )
394
+ const element = getByText("Align override attempt")
395
+ expect(element.props.className).toContain("text-left")
396
+ expect(element.props.className).not.toContain("text-center")
397
+ })
398
+
399
+ it("rejects text-decoration override", () => {
400
+ const { getByText } = render(
401
+ <F0Text decoration="none" className="underline">
402
+ Decoration override attempt
403
+ </F0Text>
404
+ )
405
+ const element = getByText("Decoration override attempt")
406
+ const classes = element.props.className.split(" ")
407
+ expect(classes).not.toContain("underline")
408
+ expect(classes).toContain("no-underline")
409
+ })
410
+
411
+ it("rejects text-transform override", () => {
412
+ const { getByText } = render(
413
+ <F0Text transform="none" className="uppercase">
414
+ Transform override attempt
415
+ </F0Text>
416
+ )
417
+ const element = getByText("Transform override attempt")
418
+ expect(element.props.className).not.toContain("uppercase")
419
+ })
420
+
421
+ it("rejects multiple typography overrides simultaneously", () => {
422
+ const { getByText } = render(
423
+ <F0Text
424
+ variant="body-sm-default"
425
+ color="default"
426
+ className="text-xl leading-loose font-bold tracking-wider text-blue-500"
427
+ >
428
+ Multi override attempt
429
+ </F0Text>
430
+ )
431
+ const element = getByText("Multi override attempt")
432
+ expect(element.props.className).toContain("text-[14px]")
433
+ expect(element.props.className).toContain("font-normal")
434
+ expect(element.props.className).toContain("leading-[20px]")
435
+ expect(element.props.className).toContain("text-f0-foreground")
436
+ expect(element.props.className).not.toContain("text-xl")
437
+ expect(element.props.className).not.toContain("font-bold")
438
+ expect(element.props.className).not.toContain("leading-loose")
439
+ expect(element.props.className).not.toContain("text-blue-500")
440
+ expect(element.props.className).not.toContain("tracking-wider")
441
+ })
442
+ })
443
+
444
+ describe("className — mixed layout + typography attempts", () => {
445
+ it("keeps layout classes, rejects typography overrides", () => {
201
446
  const { getByText } = render(
202
447
  <F0Text
203
- {...(propsWithClassName as React.ComponentProps<typeof F0Text>)}
448
+ variant="heading-md"
449
+ className="mt-4 flex-1 p-2 text-xl font-bold text-red-500"
204
450
  >
205
- Text with className attempt
451
+ Mixed attempt
206
452
  </F0Text>
207
453
  )
208
- const element = getByText("Text with className attempt")
454
+ const element = getByText("Mixed attempt")
455
+ expect(element.props.className).toContain("mt-4")
456
+ expect(element.props.className).toContain("p-2")
457
+ expect(element.props.className).toContain("flex-1")
458
+ expect(element.props.className).toContain("text-[20px]")
459
+ expect(element.props.className).toContain("font-semibold")
460
+ expect(element.props.className).toContain("text-f0-foreground")
209
461
  expect(element.props.className).not.toContain("font-bold")
210
462
  expect(element.props.className).not.toContain("text-red-500")
463
+ expect(element.props.className).not.toContain("text-xl")
211
464
  })
212
465
 
213
- it("handles ref forwarding", () => {
214
- const ref = React.createRef<RNText>()
215
- render(<F0Text ref={ref}>Ref text</F0Text>)
216
- expect(ref.current).toBeTruthy()
466
+ it("handles real-world card pattern without View wrapper", () => {
467
+ const { getByText } = render(
468
+ <F0Text variant="heading-sm" className="mb-2">
469
+ Card Title
470
+ </F0Text>
471
+ )
472
+ const element = getByText("Card Title")
473
+ expect(element.props.className).toContain("mb-2")
474
+ expect(element.props.className).toContain("text-[16px]")
475
+ expect(element.props.className).toContain("font-semibold")
217
476
  })
218
477
 
219
- it("renders nested children", () => {
478
+ it("handles icon + text row pattern", () => {
220
479
  const { getByText } = render(
221
- <F0Text>
222
- Parent <F0Text variant="body-xs-medium">nested</F0Text> text
480
+ <F0Text variant="body-sm-default" className="ml-2 flex-1">
481
+ Row item text
223
482
  </F0Text>
224
483
  )
225
- expect(getByText("nested")).toBeTruthy()
484
+ const element = getByText("Row item text")
485
+ expect(element.props.className).toContain("flex-1")
486
+ expect(element.props.className).toContain("ml-2")
487
+ expect(element.props.className).toContain("text-[14px]")
488
+ expect(element.props.className).toContain("font-normal")
489
+ })
490
+ })
491
+
492
+ describe("className — edge cases", () => {
493
+ it("works correctly when className is undefined", () => {
494
+ const { getByText } = render(<F0Text>No className</F0Text>)
495
+ const element = getByText("No className")
496
+ expect(element.props.className).toContain("text-[14px]")
497
+ expect(element.props.className).toContain("font-normal")
498
+ expect(element.props.className).toContain("text-f0-foreground")
499
+ })
500
+
501
+ it("works correctly when className is empty string", () => {
502
+ const { getByText } = render(
503
+ <F0Text className="">Empty className</F0Text>
504
+ )
505
+ const element = getByText("Empty className")
506
+ expect(element.props.className).toContain("text-[14px]")
507
+ expect(element.props.className).toContain("font-normal")
508
+ })
509
+
510
+ it("handles className with extra whitespace", () => {
511
+ const { getByText } = render(
512
+ <F0Text className=" mt-4 mb-2 ">Whitespace className</F0Text>
513
+ )
514
+ const element = getByText("Whitespace className")
515
+ expect(element.props.className).toContain("mt-4")
516
+ expect(element.props.className).toContain("mb-2")
517
+ })
518
+
519
+ it("still blocks style prop even when className is allowed", () => {
520
+ const propsWithBoth = {
521
+ style: { color: "red", marginTop: 10 },
522
+ className: "mt-4",
523
+ children: "Both props",
524
+ }
525
+ const { getByText } = render(
526
+ <F0Text {...(propsWithBoth as React.ComponentProps<typeof F0Text>)}>
527
+ Both props
528
+ </F0Text>
529
+ )
530
+ const element = getByText("Both props")
531
+ expect(element.props.style).toBeUndefined()
532
+ expect(element.props.className).toContain("mt-4")
533
+ })
534
+
535
+ it("produces correct output without className", () => {
536
+ const { getByText } = render(
537
+ <F0Text variant="heading-lg" color="accent" align="center">
538
+ No className
539
+ </F0Text>
540
+ )
541
+ const element = getByText("No className")
542
+ expect(element.props.className).toContain("no-underline")
543
+ expect(element.props.className).toContain("normal-case")
544
+ expect(element.props.className).toContain("text-[24px]")
545
+ expect(element.props.className).toContain("leading-[32px]")
546
+ expect(element.props.className).toContain("tracking-[-0.2px]")
547
+ expect(element.props.className).toContain("font-semibold")
548
+ expect(element.props.className).toContain("text-f0-foreground-accent")
549
+ expect(element.props.className).toContain("text-center")
226
550
  })
227
551
  })
228
552
  })