@harkenapp/sdk-react-native 0.0.1-alpha.1 → 0.0.1-alpha.2

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 (278) hide show
  1. package/README.md +44 -7
  2. package/app.plugin.cjs +12 -17
  3. package/dist/__mocks__/async-storage.d.ts +16 -0
  4. package/dist/__mocks__/async-storage.d.ts.map +1 -0
  5. package/dist/__mocks__/async-storage.js +39 -0
  6. package/dist/__mocks__/async-storage.js.map +1 -0
  7. package/dist/__mocks__/expo-document-picker.d.ts +26 -0
  8. package/dist/__mocks__/expo-document-picker.d.ts.map +1 -0
  9. package/dist/__mocks__/expo-document-picker.js +25 -0
  10. package/dist/__mocks__/expo-document-picker.js.map +1 -0
  11. package/dist/__mocks__/expo-file-system.d.ts +42 -0
  12. package/dist/__mocks__/expo-file-system.d.ts.map +1 -0
  13. package/dist/__mocks__/expo-file-system.js +37 -0
  14. package/dist/__mocks__/expo-file-system.js.map +1 -0
  15. package/dist/__mocks__/expo-image-picker.d.ts +30 -0
  16. package/dist/__mocks__/expo-image-picker.d.ts.map +1 -0
  17. package/dist/__mocks__/expo-image-picker.js +30 -0
  18. package/dist/__mocks__/expo-image-picker.js.map +1 -0
  19. package/dist/__mocks__/expo-secure-store.d.ts +15 -0
  20. package/dist/__mocks__/expo-secure-store.d.ts.map +1 -0
  21. package/dist/__mocks__/expo-secure-store.js +30 -0
  22. package/dist/__mocks__/expo-secure-store.js.map +1 -0
  23. package/dist/__mocks__/react-native.d.ts +73 -0
  24. package/dist/__mocks__/react-native.d.ts.map +1 -0
  25. package/dist/__mocks__/react-native.js +45 -0
  26. package/dist/__mocks__/react-native.js.map +1 -0
  27. package/dist/api/client.d.ts +8 -8
  28. package/dist/api/client.d.ts.map +1 -1
  29. package/dist/api/client.js +17 -19
  30. package/dist/api/client.js.map +1 -1
  31. package/dist/api/client.test.d.ts +2 -0
  32. package/dist/api/client.test.d.ts.map +1 -0
  33. package/dist/api/client.test.js +417 -0
  34. package/dist/api/client.test.js.map +1 -0
  35. package/dist/api/errors.d.ts +3 -3
  36. package/dist/api/errors.d.ts.map +1 -1
  37. package/dist/api/errors.js +3 -3
  38. package/dist/api/errors.js.map +1 -1
  39. package/dist/api/errors.test.d.ts +2 -0
  40. package/dist/api/errors.test.d.ts.map +1 -0
  41. package/dist/api/errors.test.js +155 -0
  42. package/dist/api/errors.test.js.map +1 -0
  43. package/dist/api/index.d.ts +6 -6
  44. package/dist/api/index.d.ts.map +1 -1
  45. package/dist/api/index.js.map +1 -1
  46. package/dist/api/retry.d.ts +1 -1
  47. package/dist/api/retry.d.ts.map +1 -1
  48. package/dist/api/retry.js.map +1 -1
  49. package/dist/api/retry.test.d.ts +2 -0
  50. package/dist/api/retry.test.d.ts.map +1 -0
  51. package/dist/api/retry.test.js +193 -0
  52. package/dist/api/retry.test.js.map +1 -0
  53. package/dist/attachments/FeedbackSheet.d.ts +36 -13
  54. package/dist/attachments/FeedbackSheet.d.ts.map +1 -1
  55. package/dist/attachments/FeedbackSheet.js +50 -30
  56. package/dist/attachments/FeedbackSheet.js.map +1 -1
  57. package/dist/attachments/index.d.ts +2 -2
  58. package/dist/components/AttachmentGrid.d.ts +12 -4
  59. package/dist/components/AttachmentGrid.d.ts.map +1 -1
  60. package/dist/components/AttachmentGrid.js +44 -34
  61. package/dist/components/AttachmentGrid.js.map +1 -1
  62. package/dist/components/AttachmentPicker.d.ts +3 -3
  63. package/dist/components/AttachmentPicker.d.ts.map +1 -1
  64. package/dist/components/AttachmentPicker.js +34 -36
  65. package/dist/components/AttachmentPicker.js.map +1 -1
  66. package/dist/components/AttachmentPreview.d.ts +10 -4
  67. package/dist/components/AttachmentPreview.d.ts.map +1 -1
  68. package/dist/components/AttachmentPreview.js +48 -34
  69. package/dist/components/AttachmentPreview.js.map +1 -1
  70. package/dist/components/CategorySelector.d.ts +3 -3
  71. package/dist/components/CategorySelector.d.ts.map +1 -1
  72. package/dist/components/CategorySelector.js +21 -27
  73. package/dist/components/CategorySelector.js.map +1 -1
  74. package/dist/components/FeedbackForm.d.ts +3 -3
  75. package/dist/components/FeedbackForm.d.ts.map +1 -1
  76. package/dist/components/FeedbackForm.js +7 -8
  77. package/dist/components/FeedbackForm.js.map +1 -1
  78. package/dist/components/FeedbackSheet.d.ts +34 -11
  79. package/dist/components/FeedbackSheet.d.ts.map +1 -1
  80. package/dist/components/FeedbackSheet.js +46 -28
  81. package/dist/components/FeedbackSheet.js.map +1 -1
  82. package/dist/components/ThemedButton.d.ts +16 -5
  83. package/dist/components/ThemedButton.d.ts.map +1 -1
  84. package/dist/components/ThemedButton.js +38 -29
  85. package/dist/components/ThemedButton.js.map +1 -1
  86. package/dist/components/ThemedText.d.ts +3 -3
  87. package/dist/components/ThemedText.d.ts.map +1 -1
  88. package/dist/components/ThemedText.js +1 -1
  89. package/dist/components/ThemedText.js.map +1 -1
  90. package/dist/components/ThemedTextInput.d.ts +11 -2
  91. package/dist/components/ThemedTextInput.d.ts.map +1 -1
  92. package/dist/components/ThemedTextInput.js +19 -9
  93. package/dist/components/ThemedTextInput.js.map +1 -1
  94. package/dist/components/UploadStatusOverlay.d.ts +11 -3
  95. package/dist/components/UploadStatusOverlay.d.ts.map +1 -1
  96. package/dist/components/UploadStatusOverlay.js +59 -76
  97. package/dist/components/UploadStatusOverlay.js.map +1 -1
  98. package/dist/components/index.d.ts +18 -18
  99. package/dist/components/index.d.ts.map +1 -1
  100. package/dist/components/index.js.map +1 -1
  101. package/dist/context/HarkenContext.d.ts +20 -15
  102. package/dist/context/HarkenContext.d.ts.map +1 -1
  103. package/dist/context/HarkenContext.js +20 -17
  104. package/dist/context/HarkenContext.js.map +1 -1
  105. package/dist/context/index.d.ts +2 -2
  106. package/dist/domain/index.d.ts +2 -2
  107. package/dist/domain/index.d.ts.map +1 -1
  108. package/dist/domain/index.js.map +1 -1
  109. package/dist/hooks/index.d.ts +5 -5
  110. package/dist/hooks/useAnonymousId.js +1 -1
  111. package/dist/hooks/useAnonymousId.test.d.ts +2 -0
  112. package/dist/hooks/useAnonymousId.test.d.ts.map +1 -0
  113. package/dist/hooks/useAnonymousId.test.js +154 -0
  114. package/dist/hooks/useAnonymousId.test.js.map +1 -0
  115. package/dist/hooks/useAttachmentPicker.d.ts +3 -3
  116. package/dist/hooks/useAttachmentPicker.js +7 -7
  117. package/dist/hooks/useAttachmentStatus.d.ts +1 -1
  118. package/dist/hooks/useAttachmentStatus.d.ts.map +1 -1
  119. package/dist/hooks/useAttachmentStatus.js.map +1 -1
  120. package/dist/hooks/useAttachmentUpload.d.ts +2 -2
  121. package/dist/hooks/useAttachmentUpload.d.ts.map +1 -1
  122. package/dist/hooks/useAttachmentUpload.js +5 -5
  123. package/dist/hooks/useAttachmentUpload.js.map +1 -1
  124. package/dist/hooks/useAttachmentUpload.test.d.ts +2 -0
  125. package/dist/hooks/useAttachmentUpload.test.d.ts.map +1 -0
  126. package/dist/hooks/useAttachmentUpload.test.js +542 -0
  127. package/dist/hooks/useAttachmentUpload.test.js.map +1 -0
  128. package/dist/hooks/useFeedback.d.ts +4 -4
  129. package/dist/hooks/useFeedback.d.ts.map +1 -1
  130. package/dist/hooks/useFeedback.js +3 -5
  131. package/dist/hooks/useFeedback.js.map +1 -1
  132. package/dist/hooks/useFeedback.test.d.ts +2 -0
  133. package/dist/hooks/useFeedback.test.d.ts.map +1 -0
  134. package/dist/hooks/useFeedback.test.js +299 -0
  135. package/dist/hooks/useFeedback.test.js.map +1 -0
  136. package/dist/hooks/useHarkenContext.d.ts +1 -1
  137. package/dist/hooks/useHarkenContext.js +1 -1
  138. package/dist/hooks/useHarkenTheme.d.ts +27 -3
  139. package/dist/hooks/useHarkenTheme.d.ts.map +1 -1
  140. package/dist/hooks/useHarkenTheme.js +26 -2
  141. package/dist/hooks/useHarkenTheme.js.map +1 -1
  142. package/dist/index.d.ts +28 -28
  143. package/dist/index.d.ts.map +1 -1
  144. package/dist/index.js.map +1 -1
  145. package/dist/services/index.d.ts +3 -3
  146. package/dist/services/index.d.ts.map +1 -1
  147. package/dist/services/index.js.map +1 -1
  148. package/dist/services/uploadQueueService.d.ts +2 -2
  149. package/dist/services/uploadQueueService.d.ts.map +1 -1
  150. package/dist/services/uploadQueueService.js +16 -17
  151. package/dist/services/uploadQueueService.js.map +1 -1
  152. package/dist/services/uploadQueueService.test.d.ts +2 -0
  153. package/dist/services/uploadQueueService.test.d.ts.map +1 -0
  154. package/dist/services/uploadQueueService.test.js +426 -0
  155. package/dist/services/uploadQueueService.test.js.map +1 -0
  156. package/dist/services/uploadQueueStorage.d.ts +1 -1
  157. package/dist/services/uploadQueueStorage.d.ts.map +1 -1
  158. package/dist/services/uploadQueueStorage.js +4 -4
  159. package/dist/services/uploadQueueStorage.js.map +1 -1
  160. package/dist/services/uploadQueueStorage.test.d.ts +2 -0
  161. package/dist/services/uploadQueueStorage.test.d.ts.map +1 -0
  162. package/dist/services/uploadQueueStorage.test.js +200 -0
  163. package/dist/services/uploadQueueStorage.test.js.map +1 -0
  164. package/dist/storage/IdentityStore.d.ts +1 -1
  165. package/dist/storage/IdentityStore.d.ts.map +1 -1
  166. package/dist/storage/IdentityStore.js.map +1 -1
  167. package/dist/storage/IdentityStore.test.d.ts +2 -0
  168. package/dist/storage/IdentityStore.test.d.ts.map +1 -0
  169. package/dist/storage/IdentityStore.test.js +176 -0
  170. package/dist/storage/IdentityStore.test.js.map +1 -0
  171. package/dist/storage/SecureStoreAdapter.d.ts +1 -1
  172. package/dist/storage/SecureStoreAdapter.test.d.ts +2 -0
  173. package/dist/storage/SecureStoreAdapter.test.d.ts.map +1 -0
  174. package/dist/storage/SecureStoreAdapter.test.js +114 -0
  175. package/dist/storage/SecureStoreAdapter.test.js.map +1 -0
  176. package/dist/storage/defaultStorage.d.ts +1 -1
  177. package/dist/storage/defaultStorage.js +4 -4
  178. package/dist/storage/defaultStorage.test.d.ts +2 -0
  179. package/dist/storage/defaultStorage.test.d.ts.map +1 -0
  180. package/dist/storage/defaultStorage.test.js +159 -0
  181. package/dist/storage/defaultStorage.test.js.map +1 -0
  182. package/dist/storage/index.d.ts +5 -5
  183. package/dist/storage/types.js +1 -1
  184. package/dist/theme/defaults.d.ts +14 -3
  185. package/dist/theme/defaults.d.ts.map +1 -1
  186. package/dist/theme/defaults.js +58 -43
  187. package/dist/theme/defaults.js.map +1 -1
  188. package/dist/theme/index.d.ts +3 -2
  189. package/dist/theme/index.d.ts.map +1 -1
  190. package/dist/theme/index.js +4 -1
  191. package/dist/theme/index.js.map +1 -1
  192. package/dist/theme/resolver.d.ts +16 -0
  193. package/dist/theme/resolver.d.ts.map +1 -0
  194. package/dist/theme/resolver.js +375 -0
  195. package/dist/theme/resolver.js.map +1 -0
  196. package/dist/theme/resolver.test.d.ts +2 -0
  197. package/dist/theme/resolver.test.d.ts.map +1 -0
  198. package/dist/theme/resolver.test.js +344 -0
  199. package/dist/theme/resolver.test.js.map +1 -0
  200. package/dist/theme/types.d.ts +378 -5
  201. package/dist/theme/types.d.ts.map +1 -1
  202. package/dist/types/config.d.ts +4 -4
  203. package/dist/types/index.d.ts +2 -2
  204. package/dist/utils/index.d.ts +1 -1
  205. package/dist/utils/uuid.d.ts.map +1 -1
  206. package/dist/utils/uuid.js +4 -5
  207. package/dist/utils/uuid.js.map +1 -1
  208. package/dist/utils/uuid.test.d.ts +2 -0
  209. package/dist/utils/uuid.test.d.ts.map +1 -0
  210. package/dist/utils/uuid.test.js +78 -0
  211. package/dist/utils/uuid.test.js.map +1 -0
  212. package/package.json +21 -13
  213. package/src/@types/expo-file-system-legacy.d.ts +3 -3
  214. package/src/__mocks__/async-storage.ts +46 -0
  215. package/src/__mocks__/expo-document-picker.ts +41 -0
  216. package/src/__mocks__/expo-file-system.ts +62 -0
  217. package/src/__mocks__/expo-image-picker.ts +48 -0
  218. package/src/__mocks__/expo-secure-store.ts +29 -0
  219. package/src/__mocks__/react-native.ts +46 -0
  220. package/src/api/client.test.ts +515 -0
  221. package/src/api/client.ts +45 -64
  222. package/src/api/errors.test.ts +193 -0
  223. package/src/api/errors.ts +7 -11
  224. package/src/api/index.ts +6 -10
  225. package/src/api/retry.test.ts +251 -0
  226. package/src/api/retry.ts +3 -6
  227. package/src/attachments/FeedbackSheet.tsx +100 -80
  228. package/src/attachments/index.ts +2 -2
  229. package/src/components/AttachmentGrid.tsx +54 -45
  230. package/src/components/AttachmentPicker.tsx +43 -54
  231. package/src/components/AttachmentPreview.tsx +51 -47
  232. package/src/components/CategorySelector.tsx +29 -35
  233. package/src/components/FeedbackForm.tsx +23 -35
  234. package/src/components/FeedbackSheet.tsx +89 -68
  235. package/src/components/ThemedButton.tsx +49 -47
  236. package/src/components/ThemedText.tsx +7 -10
  237. package/src/components/ThemedTextInput.tsx +23 -13
  238. package/src/components/UploadStatusOverlay.tsx +66 -89
  239. package/src/components/index.ts +18 -21
  240. package/src/context/HarkenContext.tsx +29 -28
  241. package/src/context/index.ts +2 -2
  242. package/src/domain/index.ts +2 -5
  243. package/src/domain/upload-queue.ts +5 -5
  244. package/src/hooks/index.ts +5 -5
  245. package/src/hooks/useAnonymousId.test.ts +189 -0
  246. package/src/hooks/useAnonymousId.ts +3 -3
  247. package/src/hooks/useAttachmentPicker.ts +12 -12
  248. package/src/hooks/useAttachmentStatus.ts +12 -16
  249. package/src/hooks/useAttachmentUpload.test.ts +632 -0
  250. package/src/hooks/useAttachmentUpload.ts +45 -54
  251. package/src/hooks/useFeedback.test.ts +376 -0
  252. package/src/hooks/useFeedback.ts +12 -14
  253. package/src/hooks/useHarkenContext.ts +4 -4
  254. package/src/hooks/useHarkenTheme.ts +30 -6
  255. package/src/index.ts +28 -52
  256. package/src/services/index.ts +3 -9
  257. package/src/services/uploadQueueService.test.ts +489 -0
  258. package/src/services/uploadQueueService.ts +40 -56
  259. package/src/services/uploadQueueStorage.test.ts +243 -0
  260. package/src/services/uploadQueueStorage.ts +7 -9
  261. package/src/storage/IdentityStore.test.ts +173 -0
  262. package/src/storage/IdentityStore.ts +4 -5
  263. package/src/storage/SecureStoreAdapter.test.ts +147 -0
  264. package/src/storage/SecureStoreAdapter.ts +1 -1
  265. package/src/storage/defaultStorage.test.ts +159 -0
  266. package/src/storage/defaultStorage.ts +6 -6
  267. package/src/storage/index.ts +5 -5
  268. package/src/storage/types.ts +1 -1
  269. package/src/theme/defaults.ts +75 -46
  270. package/src/theme/index.ts +15 -2
  271. package/src/theme/resolver.test.ts +411 -0
  272. package/src/theme/resolver.ts +446 -0
  273. package/src/theme/types.ts +453 -15
  274. package/src/types/config.ts +4 -4
  275. package/src/types/index.ts +2 -2
  276. package/src/utils/index.ts +1 -1
  277. package/src/utils/uuid.test.ts +85 -0
  278. package/src/utils/uuid.ts +4 -7
@@ -1,9 +1,9 @@
1
- import React from 'react';
2
- import { View, Pressable } from 'react-native';
3
- import type { ViewStyle, StyleProp } from 'react-native';
4
- import { useHarkenTheme } from '../hooks';
5
- import { ThemedText } from './ThemedText';
6
- import type { FeedbackCategory } from '../types';
1
+ import React from "react";
2
+ import { View, Pressable } from "react-native";
3
+ import type { ViewStyle, StyleProp } from "react-native";
4
+ import { useHarkenTheme } from "../hooks";
5
+ import { ThemedText } from "./ThemedText";
6
+ import type { FeedbackCategory } from "../types";
7
7
 
8
8
  export interface CategoryOption {
9
9
  value: FeedbackCategory;
@@ -14,10 +14,10 @@ export interface CategoryOption {
14
14
  }
15
15
 
16
16
  export const DEFAULT_CATEGORIES: CategoryOption[] = [
17
- { value: 'bug', label: 'Bug', emoji: '🐛' },
18
- { value: 'idea', label: 'Idea', emoji: '💡' },
19
- { value: 'ux', label: 'UX', emoji: '' },
20
- { value: 'other', label: 'Other', emoji: '💬' },
17
+ { value: "bug", label: "Bug", emoji: "🐛" },
18
+ { value: "idea", label: "Idea", emoji: "💡" },
19
+ { value: "ux", label: "UX", emoji: "" },
20
+ { value: "other", label: "Other", emoji: "💬" },
21
21
  ];
22
22
 
23
23
  export interface CategorySelectorProps {
@@ -100,11 +100,12 @@ export function CategorySelector({
100
100
  selectedChipStyle,
101
101
  }: CategorySelectorProps): React.JSX.Element {
102
102
  const theme = useHarkenTheme();
103
+ const { chip } = theme.components;
103
104
 
104
105
  const containerStyle: ViewStyle = {
105
- flexDirection: 'row',
106
- flexWrap: 'wrap',
107
- gap: theme.spacing.sm,
106
+ flexDirection: "row",
107
+ flexWrap: "wrap",
108
+ gap: chip.gap,
108
109
  };
109
110
 
110
111
  return (
@@ -123,22 +124,18 @@ export function CategorySelector({
123
124
  }
124
125
 
125
126
  const baseChipStyle: ViewStyle = {
126
- flexDirection: 'row',
127
- alignItems: 'center',
128
- paddingVertical: theme.spacing.sm,
129
- paddingHorizontal: theme.spacing.md,
130
- borderRadius: theme.radii.full,
127
+ flexDirection: "row",
128
+ alignItems: "center",
129
+ paddingVertical: chip.paddingVertical,
130
+ paddingHorizontal: chip.paddingHorizontal,
131
+ borderRadius: chip.radius,
131
132
  borderWidth: 1,
132
- borderColor: isSelected ? theme.colors.primary : theme.colors.border,
133
- backgroundColor: isSelected
134
- ? theme.colors.primary
135
- : theme.colors.backgroundSecondary,
136
- opacity: disabled ? 0.6 : 1,
133
+ borderColor: isSelected ? chip.borderSelected : chip.border,
134
+ backgroundColor: isSelected ? chip.backgroundSelected : chip.background,
135
+ opacity: disabled ? theme.opacity.disabled : 1,
137
136
  };
138
137
 
139
- const textColor = isSelected
140
- ? theme.colors.textOnPrimary
141
- : theme.colors.text;
138
+ const textColor = isSelected ? chip.textSelected : chip.text;
142
139
 
143
140
  return (
144
141
  <Pressable
@@ -149,19 +146,16 @@ export function CategorySelector({
149
146
  baseChipStyle,
150
147
  chipStyle,
151
148
  isSelected && selectedChipStyle,
152
- pressed && !disabled && {
153
- opacity: 0.8,
154
- },
149
+ pressed &&
150
+ !disabled && {
151
+ opacity: theme.opacity.pressed,
152
+ },
155
153
  ]}
156
154
  >
157
155
  {category.icon ? (
158
- <View style={{ marginRight: theme.spacing.xs }}>
159
- {category.icon}
160
- </View>
156
+ <View style={{ marginRight: theme.spacing.xs }}>{category.icon}</View>
161
157
  ) : category.emoji ? (
162
- <ThemedText style={{ marginRight: theme.spacing.xs }}>
163
- {category.emoji}
164
- </ThemedText>
158
+ <ThemedText style={{ marginRight: theme.spacing.xs }}>{category.emoji}</ThemedText>
165
159
  ) : null}
166
160
  <ThemedText variant="label" color={textColor}>
167
161
  {category.label}
@@ -1,13 +1,13 @@
1
- import React, { useState, useCallback } from 'react';
2
- import { View, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
3
- import type { ViewStyle } from 'react-native';
4
- import { useHarkenTheme } from '../hooks';
5
- import { ThemedText } from './ThemedText';
6
- import { ThemedTextInput } from './ThemedTextInput';
7
- import { ThemedButton } from './ThemedButton';
8
- import { CategorySelector } from './CategorySelector';
9
- import type { CategoryOption } from './CategorySelector';
10
- import type { FeedbackCategory } from '../types';
1
+ import React, { useState, useCallback } from "react";
2
+ import { View, KeyboardAvoidingView, Platform, ScrollView } from "react-native";
3
+ import type { ViewStyle } from "react-native";
4
+ import { useHarkenTheme } from "../hooks";
5
+ import { ThemedText } from "./ThemedText";
6
+ import { ThemedTextInput } from "./ThemedTextInput";
7
+ import { ThemedButton } from "./ThemedButton";
8
+ import { CategorySelector } from "./CategorySelector";
9
+ import type { CategoryOption } from "./CategorySelector";
10
+ import type { FeedbackCategory } from "../types";
11
11
 
12
12
  export interface FeedbackFormData {
13
13
  message: string;
@@ -59,10 +59,10 @@ export interface FeedbackFormProps {
59
59
  export function FeedbackForm({
60
60
  onSubmit,
61
61
  onCancel,
62
- title = 'Send Feedback',
63
- placeholder = 'What would you like to share?',
64
- submitLabel = 'Submit',
65
- cancelLabel = 'Cancel',
62
+ title = "Send Feedback",
63
+ placeholder = "What would you like to share?",
64
+ submitLabel = "Submit",
65
+ cancelLabel = "Cancel",
66
66
  categories,
67
67
  requireCategory = false,
68
68
  minMessageLength = 1,
@@ -72,7 +72,7 @@ export function FeedbackForm({
72
72
  }: FeedbackFormProps): React.JSX.Element {
73
73
  const theme = useHarkenTheme();
74
74
 
75
- const [message, setMessage] = useState(initialValues?.message ?? '');
75
+ const [message, setMessage] = useState(initialValues?.message ?? "");
76
76
  const [category, setCategory] = useState<FeedbackCategory | null>(
77
77
  initialValues?.category ?? null
78
78
  );
@@ -80,8 +80,7 @@ export function FeedbackForm({
80
80
 
81
81
  const trimmedMessage = message.trim();
82
82
  const isMessageValid =
83
- trimmedMessage.length >= minMessageLength &&
84
- trimmedMessage.length <= maxMessageLength;
83
+ trimmedMessage.length >= minMessageLength && trimmedMessage.length <= maxMessageLength;
85
84
  const isCategoryValid = !requireCategory || category !== null;
86
85
  const canSubmit = isMessageValid && isCategoryValid && !loading && !isSubmitting;
87
86
 
@@ -110,7 +109,7 @@ export function FeedbackForm({
110
109
  };
111
110
 
112
111
  const buttonRowStyle: ViewStyle = {
113
- flexDirection: 'row',
112
+ flexDirection: "row",
114
113
  gap: theme.spacing.sm,
115
114
  marginTop: theme.spacing.md,
116
115
  };
@@ -120,13 +119,10 @@ export function FeedbackForm({
120
119
 
121
120
  return (
122
121
  <KeyboardAvoidingView
123
- behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
122
+ behavior={Platform.OS === "ios" ? "padding" : "height"}
124
123
  style={{ flex: 1 }}
125
124
  >
126
- <ScrollView
127
- contentContainerStyle={{ flexGrow: 1 }}
128
- keyboardShouldPersistTaps="handled"
129
- >
125
+ <ScrollView contentContainerStyle={{ flexGrow: 1 }} keyboardShouldPersistTaps="handled">
130
126
  <View style={containerStyle}>
131
127
  {/* Title */}
132
128
  <View style={sectionStyle}>
@@ -135,12 +131,8 @@ export function FeedbackForm({
135
131
 
136
132
  {/* Category selector */}
137
133
  <View style={sectionStyle}>
138
- <ThemedText
139
- variant="label"
140
- secondary
141
- style={{ marginBottom: theme.spacing.sm }}
142
- >
143
- Category{requireCategory ? '' : ' (optional)'}
134
+ <ThemedText variant="label" secondary style={{ marginBottom: theme.spacing.sm }}>
135
+ Category{requireCategory ? "" : " (optional)"}
144
136
  </ThemedText>
145
137
  <CategorySelector
146
138
  value={category}
@@ -152,11 +144,7 @@ export function FeedbackForm({
152
144
 
153
145
  {/* Message input */}
154
146
  <View style={sectionStyle}>
155
- <ThemedText
156
- variant="label"
157
- secondary
158
- style={{ marginBottom: theme.spacing.sm }}
159
- >
147
+ <ThemedText variant="label" secondary style={{ marginBottom: theme.spacing.sm }}>
160
148
  Message
161
149
  </ThemedText>
162
150
  <ThemedTextInput
@@ -178,7 +166,7 @@ export function FeedbackForm({
178
166
  ? theme.colors.error
179
167
  : theme.colors.textSecondary
180
168
  }
181
- style={{ marginTop: theme.spacing.xs, textAlign: 'right' }}
169
+ style={{ marginTop: theme.spacing.xs, textAlign: "right" }}
182
170
  >
183
171
  {characterCount}/{maxMessageLength}
184
172
  </ThemedText>
@@ -1,22 +1,16 @@
1
- import React, { useState, useCallback } from 'react';
2
- import {
3
- View,
4
- KeyboardAvoidingView,
5
- Platform,
6
- ScrollView,
7
- Alert,
8
- } from 'react-native';
9
- import type { ViewStyle } from 'react-native';
10
- import type { components } from '../types/index.js';
11
- import { useHarkenTheme, useFeedback } from '../hooks';
12
- import { ThemedText } from './ThemedText';
13
- import { ThemedTextInput } from './ThemedTextInput';
14
- import { ThemedButton } from './ThemedButton';
15
- import { CategorySelector, DEFAULT_CATEGORIES } from './CategorySelector';
16
- import type { CategoryOption } from './CategorySelector';
17
- import type { FeedbackCategory } from '../types';
1
+ import React, { useState, useCallback } from "react";
2
+ import { View, KeyboardAvoidingView, Platform, ScrollView, Alert } from "react-native";
3
+ import type { ViewStyle, StyleProp } from "react-native";
4
+ import type { components } from "../types/index.js";
5
+ import { useHarkenTheme, useFeedback } from "../hooks";
6
+ import { ThemedText } from "./ThemedText";
7
+ import { ThemedTextInput } from "./ThemedTextInput";
8
+ import { ThemedButton } from "./ThemedButton";
9
+ import { CategorySelector, DEFAULT_CATEGORIES } from "./CategorySelector";
10
+ import type { CategoryOption } from "./CategorySelector";
11
+ import type { FeedbackCategory } from "../types";
18
12
 
19
- type FeedbackSubmissionResponse = components['schemas']['FeedbackSubmissionResponse'];
13
+ type FeedbackSubmissionResponse = components["schemas"]["FeedbackSubmissionResponse"];
20
14
 
21
15
  export interface FeedbackSheetProps {
22
16
  /** Called when feedback is successfully submitted */
@@ -26,7 +20,7 @@ export interface FeedbackSheetProps {
26
20
  /** Called when user cancels/dismisses the form */
27
21
  onCancel?: () => void;
28
22
 
29
- /** Title text */
23
+ /** Title text. Set to empty string to hide title section entirely. */
30
24
  title?: string;
31
25
  /** Placeholder text for message input */
32
26
  placeholder?: string;
@@ -52,10 +46,21 @@ export interface FeedbackSheetProps {
52
46
  /** Whether to clear form on success. @default true */
53
47
  clearOnSuccess?: boolean;
54
48
 
55
- /** Container style override */
56
- containerStyle?: ViewStyle;
57
- /** Form content style override */
58
- formStyle?: ViewStyle;
49
+ /**
50
+ * Layout mode for the container.
51
+ * - 'flex': Uses flex: 1 (default, requires parent with explicit height)
52
+ * - 'auto': Content determines height (for bottom sheet modal embedding)
53
+ */
54
+ layout?: "flex" | "auto";
55
+
56
+ /** Container style override (outer KeyboardAvoidingView) */
57
+ containerStyle?: StyleProp<ViewStyle>;
58
+ /** Content style override (inner ScrollView content) */
59
+ contentStyle?: StyleProp<ViewStyle>;
60
+ /**
61
+ * @deprecated Use `contentStyle` instead
62
+ */
63
+ formStyle?: StyleProp<ViewStyle>;
59
64
  }
60
65
 
61
66
  /**
@@ -70,11 +75,24 @@ export interface FeedbackSheetProps {
70
75
  *
71
76
  * For attachment support, import from '@harkenapp/sdk-react-native/attachments'.
72
77
  *
78
+ * Uses the following theme tokens:
79
+ * - `colors.formBackground` for background
80
+ * - `spacing.formPadding` for padding
81
+ * - `spacing.sectionGap` for section gaps
82
+ * - `radii.form` for border radius
83
+ *
73
84
  * @example
74
85
  * ```tsx
75
86
  * // Minimal usage
76
87
  * <FeedbackSheet onSuccess={() => navigation.goBack()} />
77
88
  *
89
+ * // For bottom sheet modal embedding
90
+ * <FeedbackSheet
91
+ * layout="auto"
92
+ * title=""
93
+ * onSuccess={() => closeModal()}
94
+ * />
95
+ *
78
96
  * // With customization
79
97
  * <FeedbackSheet
80
98
  * title="Report a Bug"
@@ -95,36 +113,37 @@ export function FeedbackSheet({
95
113
  onSuccess,
96
114
  onError,
97
115
  onCancel,
98
- title = 'Send Feedback',
99
- placeholder = 'What would you like to share?',
100
- submitLabel = 'Submit',
101
- cancelLabel = 'Cancel',
116
+ title = "Send Feedback",
117
+ placeholder = "What would you like to share?",
118
+ submitLabel = "Submit",
119
+ cancelLabel = "Cancel",
102
120
  categories = DEFAULT_CATEGORIES,
103
121
  requireCategory = false,
104
122
  minMessageLength = 1,
105
123
  maxMessageLength = 5000,
106
- successMessage = 'Thank you for your feedback!',
124
+ successMessage = "Thank you for your feedback!",
107
125
  showSuccessAlert = true,
108
126
  clearOnSuccess = true,
127
+ layout = "flex",
109
128
  containerStyle,
129
+ contentStyle,
110
130
  formStyle,
111
131
  }: FeedbackSheetProps): React.JSX.Element {
112
132
  const theme = useHarkenTheme();
113
- const { submitFeedback, isSubmitting, error, clearError, isInitializing } =
114
- useFeedback();
133
+ const { form } = theme.components;
134
+ const { submitFeedback, isSubmitting, error, clearError, isInitializing } = useFeedback();
115
135
 
116
- const [message, setMessage] = useState('');
136
+ const [message, setMessage] = useState("");
117
137
  const [category, setCategory] = useState<FeedbackCategory | null>(null);
118
138
 
119
139
  const trimmedMessage = message.trim();
120
140
  const isMessageValid =
121
- trimmedMessage.length >= minMessageLength &&
122
- trimmedMessage.length <= maxMessageLength;
141
+ trimmedMessage.length >= minMessageLength && trimmedMessage.length <= maxMessageLength;
123
142
  const isCategoryValid = !requireCategory || category !== null;
124
143
  const canSubmit = isMessageValid && isCategoryValid && !isSubmitting && !isInitializing;
125
144
 
126
145
  const resetForm = useCallback(() => {
127
- setMessage('');
146
+ setMessage("");
128
147
  setCategory(null);
129
148
  clearError();
130
149
  }, [clearError]);
@@ -137,13 +156,13 @@ export function FeedbackSheet({
137
156
  try {
138
157
  const result = await submitFeedback({
139
158
  message: trimmedMessage,
140
- category: category ?? 'other',
159
+ category: category ?? "other",
141
160
  });
142
161
 
143
162
  if (showSuccessAlert && successMessage) {
144
- Alert.alert('Success', successMessage, [
163
+ Alert.alert("Success", successMessage, [
145
164
  {
146
- text: 'OK',
165
+ text: "OK",
147
166
  onPress: () => {
148
167
  if (clearOnSuccess) {
149
168
  resetForm();
@@ -160,8 +179,8 @@ export function FeedbackSheet({
160
179
  }
161
180
  } catch (e) {
162
181
  const errorMessage =
163
- e instanceof Error ? e.message : 'Failed to submit feedback. Please try again.';
164
- Alert.alert('Submission Failed', errorMessage);
182
+ e instanceof Error ? e.message : "Failed to submit feedback. Please try again.";
183
+ Alert.alert("Submission Failed", errorMessage);
165
184
  onError?.(e instanceof Error ? e : new Error(errorMessage));
166
185
  }
167
186
  }, [
@@ -184,21 +203,22 @@ export function FeedbackSheet({
184
203
  }, [resetForm, onCancel]);
185
204
 
186
205
  const baseContainerStyle: ViewStyle = {
187
- flex: 1,
188
- backgroundColor: theme.colors.background,
206
+ ...(layout === "flex" ? { flex: 1 } : {}),
207
+ backgroundColor: form.background,
208
+ borderRadius: form.radius,
189
209
  };
190
210
 
191
- const contentStyle: ViewStyle = {
192
- flexGrow: 1,
193
- padding: theme.spacing.lg,
211
+ const scrollContentStyle: ViewStyle = {
212
+ ...(layout === "flex" ? { flexGrow: 1 } : {}),
213
+ padding: form.padding,
194
214
  };
195
215
 
196
216
  const sectionStyle: ViewStyle = {
197
- marginBottom: theme.spacing.lg,
217
+ marginBottom: form.sectionGap,
198
218
  };
199
219
 
200
220
  const buttonRowStyle: ViewStyle = {
201
- flexDirection: 'row',
221
+ flexDirection: "row",
202
222
  gap: theme.spacing.sm,
203
223
  marginTop: theme.spacing.md,
204
224
  };
@@ -206,9 +226,18 @@ export function FeedbackSheet({
206
226
  const characterCount = trimmedMessage.length;
207
227
  const showCharacterWarning = characterCount > maxMessageLength * 0.9;
208
228
 
229
+ // Support deprecated formStyle prop
230
+ const effectiveContentStyle = contentStyle ?? formStyle;
231
+
209
232
  if (isInitializing) {
210
233
  return (
211
- <View style={[baseContainerStyle, containerStyle, { justifyContent: 'center', alignItems: 'center' }]}>
234
+ <View
235
+ style={[
236
+ baseContainerStyle,
237
+ containerStyle,
238
+ { justifyContent: "center", alignItems: "center" },
239
+ ]}
240
+ >
212
241
  <ThemedText variant="body" secondary>
213
242
  Initializing...
214
243
  </ThemedText>
@@ -218,26 +247,24 @@ export function FeedbackSheet({
218
247
 
219
248
  return (
220
249
  <KeyboardAvoidingView
221
- behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
250
+ behavior={Platform.OS === "ios" ? "padding" : "height"}
222
251
  style={[baseContainerStyle, containerStyle]}
223
252
  >
224
253
  <ScrollView
225
- contentContainerStyle={[contentStyle, formStyle]}
254
+ contentContainerStyle={[scrollContentStyle, effectiveContentStyle]}
226
255
  keyboardShouldPersistTaps="handled"
227
256
  >
228
- {/* Title */}
229
- <View style={sectionStyle}>
230
- <ThemedText variant="title">{title}</ThemedText>
231
- </View>
257
+ {/* Title - only render if provided */}
258
+ {title ? (
259
+ <View style={sectionStyle}>
260
+ <ThemedText variant="title">{title}</ThemedText>
261
+ </View>
262
+ ) : null}
232
263
 
233
264
  {/* Category selector */}
234
265
  <View style={sectionStyle}>
235
- <ThemedText
236
- variant="label"
237
- secondary
238
- style={{ marginBottom: theme.spacing.sm }}
239
- >
240
- Category{requireCategory ? '' : ' (optional)'}
266
+ <ThemedText variant="label" secondary style={{ marginBottom: theme.spacing.sm }}>
267
+ Category{requireCategory ? "" : " (optional)"}
241
268
  </ThemedText>
242
269
  <CategorySelector
243
270
  value={category}
@@ -249,11 +276,7 @@ export function FeedbackSheet({
249
276
 
250
277
  {/* Message input */}
251
278
  <View style={sectionStyle}>
252
- <ThemedText
253
- variant="label"
254
- secondary
255
- style={{ marginBottom: theme.spacing.sm }}
256
- >
279
+ <ThemedText variant="label" secondary style={{ marginBottom: theme.spacing.sm }}>
257
280
  Message
258
281
  </ThemedText>
259
282
  <ThemedTextInput
@@ -271,11 +294,9 @@ export function FeedbackSheet({
271
294
  <ThemedText
272
295
  variant="caption"
273
296
  color={
274
- characterCount > maxMessageLength
275
- ? theme.colors.error
276
- : theme.colors.textSecondary
297
+ characterCount > maxMessageLength ? theme.colors.error : theme.colors.textSecondary
277
298
  }
278
- style={{ marginTop: theme.spacing.xs, textAlign: 'right' }}
299
+ style={{ marginTop: theme.spacing.xs, textAlign: "right" }}
279
300
  >
280
301
  {characterCount}/{maxMessageLength}
281
302
  </ThemedText>
@@ -1,16 +1,12 @@
1
- import React from 'react';
2
- import {
3
- Pressable,
4
- ActivityIndicator,
5
- StyleSheet,
6
- } from 'react-native';
7
- import type { PressableProps, ViewStyle, StyleProp } from 'react-native';
8
- import { useHarkenTheme } from '../hooks';
9
- import { ThemedText } from './ThemedText';
1
+ import React from "react";
2
+ import { Pressable, ActivityIndicator, StyleSheet } from "react-native";
3
+ import type { PressableProps, ViewStyle, TextStyle, StyleProp } from "react-native";
4
+ import { useHarkenTheme } from "../hooks";
5
+ import { ThemedText } from "./ThemedText";
10
6
 
11
- export type ButtonVariant = 'primary' | 'secondary' | 'ghost';
7
+ export type ButtonVariant = "primary" | "secondary" | "ghost";
12
8
 
13
- export interface ThemedButtonProps extends Omit<PressableProps, 'children' | 'style'> {
9
+ export interface ThemedButtonProps extends Omit<PressableProps, "children" | "style"> {
14
10
  /** Button text */
15
11
  title: string;
16
12
  /** Button variant */
@@ -24,36 +20,47 @@ export interface ThemedButtonProps extends Omit<PressableProps, 'children' | 'st
24
20
  * Note: Function styles are not supported; use static StyleProp<ViewStyle>.
25
21
  */
26
22
  style?: StyleProp<ViewStyle>;
23
+ /** Additional styles for the button text */
24
+ textStyle?: StyleProp<TextStyle>;
27
25
  }
28
26
 
29
27
  /**
30
28
  * Themed button component with Harken styling.
29
+ *
30
+ * Uses the following theme tokens:
31
+ * - `colors.buttonPrimary*` for primary variant
32
+ * - `colors.buttonSecondary*` for secondary variant
33
+ * - `colors.buttonGhostText` for ghost variant
34
+ * - `spacing.buttonPadding*` for padding
35
+ * - `radii.button` for border radius
36
+ * - `sizing.buttonMinHeight` for minimum height
37
+ * - `opacity.disabled` for disabled state
31
38
  */
32
39
  export function ThemedButton({
33
40
  title,
34
- variant = 'primary',
41
+ variant = "primary",
35
42
  loading = false,
36
43
  fullWidth = false,
37
44
  disabled,
38
45
  style,
46
+ textStyle,
39
47
  ...props
40
48
  }: ThemedButtonProps): React.JSX.Element {
41
49
  const theme = useHarkenTheme();
50
+ const { button } = theme.components;
42
51
 
43
52
  const getBackgroundColor = (pressed: boolean): string => {
44
53
  if (disabled) {
45
- return variant === 'primary'
46
- ? theme.colors.border
47
- : 'transparent';
54
+ return variant === "primary" ? theme.colors.border : "transparent";
48
55
  }
49
56
 
50
57
  switch (variant) {
51
- case 'primary':
52
- return pressed ? theme.colors.primaryPressed : theme.colors.primary;
53
- case 'secondary':
54
- return pressed ? theme.colors.border : theme.colors.backgroundSecondary;
55
- case 'ghost':
56
- return pressed ? theme.colors.backgroundSecondary : 'transparent';
58
+ case "primary":
59
+ return pressed ? button.primary.backgroundPressed : button.primary.background;
60
+ case "secondary":
61
+ return pressed ? theme.colors.border : button.secondary.background;
62
+ case "ghost":
63
+ return pressed ? theme.colors.surface : "transparent";
57
64
  }
58
65
  };
59
66
 
@@ -63,20 +70,21 @@ export function ThemedButton({
63
70
  }
64
71
 
65
72
  switch (variant) {
66
- case 'primary':
67
- return theme.colors.textOnPrimary;
68
- case 'secondary':
69
- case 'ghost':
70
- return theme.colors.text;
73
+ case "primary":
74
+ return button.primary.text;
75
+ case "secondary":
76
+ return button.secondary.text;
77
+ case "ghost":
78
+ return button.ghost.text;
71
79
  }
72
80
  };
73
81
 
74
82
  const getBorderColor = (): string => {
75
83
  switch (variant) {
76
- case 'secondary':
77
- return theme.colors.border;
84
+ case "secondary":
85
+ return button.secondary.border;
78
86
  default:
79
- return 'transparent';
87
+ return "transparent";
80
88
  }
81
89
  };
82
90
 
@@ -89,20 +97,20 @@ export function ThemedButton({
89
97
  style={({ pressed }) => {
90
98
  const baseStyle: ViewStyle = {
91
99
  backgroundColor: getBackgroundColor(pressed),
92
- borderWidth: variant === 'secondary' ? 1 : 0,
100
+ borderWidth: variant === "secondary" ? 1 : 0,
93
101
  borderColor: getBorderColor(),
94
- borderRadius: theme.radii.md,
95
- paddingVertical: theme.spacing.sm + 4,
96
- paddingHorizontal: theme.spacing.md,
97
- alignItems: 'center',
98
- justifyContent: 'center',
99
- flexDirection: 'row',
100
- minHeight: 48,
101
- opacity: disabled ? 0.6 : 1,
102
+ borderRadius: button.radius,
103
+ paddingVertical: button.paddingVertical + 4,
104
+ paddingHorizontal: button.paddingHorizontal,
105
+ alignItems: "center",
106
+ justifyContent: "center",
107
+ flexDirection: "row",
108
+ minHeight: button.minHeight,
109
+ opacity: disabled ? theme.opacity.disabled : 1,
102
110
  };
103
111
 
104
112
  if (fullWidth) {
105
- baseStyle.width = '100%';
113
+ baseStyle.width = "100%";
106
114
  }
107
115
 
108
116
  return [baseStyle, flattenedStyle];
@@ -110,15 +118,9 @@ export function ThemedButton({
110
118
  {...props}
111
119
  >
112
120
  {loading ? (
113
- <ActivityIndicator
114
- color={getTextColor()}
115
- size="small"
116
- />
121
+ <ActivityIndicator color={getTextColor()} size="small" />
117
122
  ) : (
118
- <ThemedText
119
- variant="label"
120
- color={getTextColor()}
121
- >
123
+ <ThemedText variant="label" color={getTextColor()} style={textStyle}>
122
124
  {title}
123
125
  </ThemedText>
124
126
  )}